mirror of
https://github.com/axllent/mailpit.git
synced 2026-03-06 16:57:02 +00:00
Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d146a23d7 | ||
|
|
a6c1bbc977 | ||
|
|
d020861559 | ||
|
|
7fd3291040 | ||
|
|
479c74500c | ||
|
|
6b6de59c47 | ||
|
|
a5de4e4f65 | ||
|
|
48f22cca1f | ||
|
|
7748846b88 | ||
|
|
497086cb65 | ||
|
|
42ecadab9e | ||
|
|
4cfde7f947 | ||
|
|
70b604e028 | ||
|
|
8c295d4754 | ||
|
|
a1c34b37e1 | ||
|
|
e37583073e | ||
|
|
4de830c490 | ||
|
|
22a4509b13 | ||
|
|
1ed06161a8 | ||
|
|
a7ee479f06 | ||
|
|
93e8884ef7 | ||
|
|
1c228cda56 | ||
|
|
119b3864b2 | ||
|
|
b9f035790d | ||
|
|
1260c2e6df | ||
|
|
3431f18a3f | ||
|
|
2fa5138b49 | ||
|
|
652fec0f64 | ||
|
|
f168e11b05 | ||
|
|
35e81e0336 | ||
|
|
7beed988e5 | ||
|
|
4eea79f0c8 | ||
|
|
39767e979c | ||
|
|
4e2f02ee0a | ||
|
|
5a04534314 | ||
|
|
6725a809d5 | ||
|
|
64a067cff9 | ||
|
|
58dbccc0a7 | ||
|
|
3ef320d277 | ||
|
|
18e95b699e | ||
|
|
fc89655b7f | ||
|
|
ff9a6ff491 | ||
|
|
adce75ab8f | ||
|
|
12903cae60 | ||
|
|
7f55511c82 | ||
|
|
309036fb6d | ||
|
|
48387c3a13 | ||
|
|
a2ab350aff | ||
|
|
c150f1ba50 | ||
|
|
48bec0c8f6 | ||
|
|
fef2628c3f | ||
|
|
e5888ede8b | ||
|
|
374a760b88 | ||
|
|
0fdfa13a38 | ||
|
|
b41df78c4f | ||
|
|
870e523c97 | ||
|
|
0b391b5c37 | ||
|
|
c01f473e79 | ||
|
|
3c27fd715b | ||
|
|
714596a13a | ||
|
|
9ae02daf1a | ||
|
|
b6750600cb | ||
|
|
78e871e9b3 | ||
|
|
8ff2a5cf6a | ||
|
|
4a88d1fc24 | ||
|
|
d4268b8ae1 | ||
|
|
1b47716f5f | ||
|
|
42e6d71415 | ||
|
|
cd5789dda2 | ||
|
|
cd2a9d433a | ||
|
|
fe0dfe41e7 | ||
|
|
bee3174c78 | ||
|
|
a3187d5499 | ||
|
|
dc7f047b9a | ||
|
|
f3bb522143 | ||
|
|
3a41d56cc6 | ||
|
|
db5d8f672a | ||
|
|
3d96b2cad0 | ||
|
|
34c1748f4b | ||
|
|
52120abefd | ||
|
|
086142e977 | ||
|
|
078f42f4ea | ||
|
|
df5ded49b8 | ||
|
|
3bd1eca2ab | ||
|
|
95b54ce8a4 | ||
|
|
eb3330939d | ||
|
|
50b5f8667a | ||
|
|
a121c08dc4 | ||
|
|
9ff9b783cc | ||
|
|
7f68ea407b | ||
|
|
9a8e7ebdf9 | ||
|
|
db7f2c1a5d | ||
|
|
2ac0b40ecf | ||
|
|
d1edbe73b4 | ||
|
|
24e23790ec | ||
|
|
bc8722d1cf | ||
|
|
b1e3e1f879 | ||
|
|
635714945e | ||
|
|
1200750111 | ||
|
|
9670c4e1d5 | ||
|
|
1e97e9e21f | ||
|
|
ca31524487 | ||
|
|
4800922f91 | ||
|
|
6884cf34fc | ||
|
|
3b75bf3fa3 | ||
|
|
b4a971f552 | ||
|
|
e77d0a750d | ||
|
|
bdf887389e | ||
|
|
fdc1b05545 | ||
|
|
316b5d7c66 | ||
|
|
4f13785174 | ||
|
|
c83acfb255 | ||
|
|
1e8f10732e | ||
|
|
40bced067e | ||
|
|
f2bce03e9e | ||
|
|
34b62bd08a | ||
|
|
9d64e53b93 | ||
|
|
16bc025fff | ||
|
|
14a61859f0 | ||
|
|
304a379c30 | ||
|
|
82b0829429 | ||
|
|
25c393d380 | ||
|
|
b66f1d0ae1 | ||
|
|
5f919cc9dd | ||
|
|
225a1e2e2a | ||
|
|
6dca57ba9b | ||
|
|
60ea473acb | ||
|
|
0d9b0cdc43 | ||
|
|
e843de6166 | ||
|
|
b6f2618b34 | ||
|
|
31c0a501e8 | ||
|
|
08288e904d | ||
|
|
dfb455c59c | ||
|
|
5e00013a8d | ||
|
|
c5a8836b7e | ||
|
|
ae73c721db |
3
.github/workflows/release-build.yml
vendored
3
.github/workflows/release-build.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- run: npm run package
|
||||
|
||||
# build the binaries
|
||||
- uses: wangyoucao577/go-release-action@v1.37
|
||||
- uses: wangyoucao577/go-release-action@v1.38
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
@@ -44,4 +44,5 @@ jobs:
|
||||
extra_files: LICENSE README.md
|
||||
md5sum: false
|
||||
overwrite: true
|
||||
retry: 5
|
||||
ldflags: -w -X "github.com/axllent/mailpit/config.Version=${{ github.ref_name }}"
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,7 +1,9 @@
|
||||
/node_modules/
|
||||
/send
|
||||
/sendmail/sendmail
|
||||
/server/ui/dist
|
||||
/Makefile
|
||||
/mailpit*
|
||||
/.idea
|
||||
*.old
|
||||
*.db
|
||||
|
||||
182
CHANGELOG.md
182
CHANGELOG.md
@@ -2,6 +2,188 @@
|
||||
|
||||
Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [v1.7.0]
|
||||
|
||||
### API
|
||||
- Ignore SMTP relay error when one of multiple recipients doesn't exist
|
||||
- Set raw message Content-Type to UTF-8
|
||||
|
||||
### Build
|
||||
- Define Vue build options in esbuild
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
- Update Go modules
|
||||
|
||||
### UI
|
||||
- Theme toggler - auto, light and dark themes
|
||||
|
||||
|
||||
## [v1.6.22]
|
||||
|
||||
### Feature
|
||||
- Clearer SMTP error messages
|
||||
|
||||
### Libs
|
||||
- Update Go modules
|
||||
- Upgrade node modules
|
||||
|
||||
|
||||
## [v1.6.21]
|
||||
|
||||
### UI
|
||||
- More accurate clickable hyperlink logic in plain text messages
|
||||
|
||||
|
||||
## [v1.6.20]
|
||||
|
||||
### Feature
|
||||
- Convert links into clickable hyperlinks in plain text message content
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
|
||||
|
||||
## [v1.6.19]
|
||||
|
||||
### Fix
|
||||
- Only display sendmail help when sendmail subcommand is invoked
|
||||
|
||||
|
||||
## [v1.6.18]
|
||||
|
||||
### API
|
||||
- Sort tags before saving
|
||||
|
||||
### UI
|
||||
- Add option to enable tag colors based on tag name hash
|
||||
- Display message tags below subject in message overview
|
||||
|
||||
|
||||
## [v1.6.17]
|
||||
|
||||
### Fix
|
||||
- Add single dash arguments support to sendmail command ([#123](https://github.com/axllent/mailpit/issues/123))
|
||||
|
||||
|
||||
## [v1.6.16]
|
||||
|
||||
### Bugfix
|
||||
- Fix sendmail/startup panic
|
||||
|
||||
|
||||
## [v1.6.15]
|
||||
|
||||
### Feature
|
||||
- Add `sendmail -bs` functionality
|
||||
|
||||
|
||||
## [v1.6.14]
|
||||
|
||||
### Feature
|
||||
- Add ability to delete or mark search results read
|
||||
- Set tags via X-Tags message header
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
|
||||
|
||||
## [v1.6.13]
|
||||
|
||||
### Feature
|
||||
- Add SMTP LOGIN authentication method for message relay
|
||||
|
||||
|
||||
## [v1.6.12]
|
||||
|
||||
### Feature
|
||||
- Add Message-Id to MessageSummary ([#116](https://github.com/axllent/mailpit/issues/116))
|
||||
|
||||
### Swagger
|
||||
- Update swagger field descriptions, add MessageID
|
||||
|
||||
|
||||
## [v1.6.11]
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
- Update Go modules
|
||||
|
||||
### UI
|
||||
- Check for secure context instead of HTTPS ([#114](https://github.com/axllent/mailpit/issues/114))
|
||||
|
||||
|
||||
## [v1.6.10]
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
- Update Go modules
|
||||
|
||||
### UI
|
||||
- Remove "Noto Color Emoji" from default bootstrap font list
|
||||
|
||||
|
||||
## [v1.6.9]
|
||||
|
||||
### API
|
||||
- Return blank 200 response for OPTIONS requests (CORS)
|
||||
|
||||
### Bugfix
|
||||
- Correctly escape JS cid regex
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
- Update Go modules
|
||||
|
||||
|
||||
## [v1.6.8]
|
||||
|
||||
### Bugfix
|
||||
- Fix Date display when message doesn't contain a Date header
|
||||
|
||||
### Feature
|
||||
- Add allowlist to filter recipients before relaying messages ([#109](https://github.com/axllent/mailpit/issues/109))
|
||||
- Add `-S` short flag for sendmail `--smtp-addr`
|
||||
|
||||
|
||||
## [v1.6.7]
|
||||
|
||||
### Bugfix
|
||||
- Fix auto-deletion cron
|
||||
|
||||
|
||||
## [v1.6.6]
|
||||
|
||||
### API
|
||||
- Set Access-Control-Allow-Headers when --api-cors is set
|
||||
- Include correct start value in search reponse
|
||||
|
||||
### Feature
|
||||
- Option to ignore duplicate Message-IDs
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
- Update Go modules
|
||||
|
||||
### Swagger
|
||||
- Update swagger field descriptions
|
||||
|
||||
### UI
|
||||
- Style Undisclosed recipients in message view
|
||||
|
||||
|
||||
## [v1.6.5]
|
||||
|
||||
### Feature
|
||||
- Add Access-Control-Allow-Methods methods when CORS origin is set
|
||||
|
||||
|
||||
## [v1.6.4]
|
||||
|
||||
### Bugfix
|
||||
- Fix UI images not displaying when multiple cid names overlap
|
||||
|
||||
|
||||
## [v1.6.3]
|
||||
|
||||
### Feature
|
||||
|
||||
22
README.md
22
README.md
@@ -24,11 +24,11 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
|
||||
- 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)
|
||||
- Optional browser notifications for new mail (HTTPS and `localhost` only)
|
||||
- Configurable automatic email pruning (default keeps the most recent 500 emails)
|
||||
- Email storage either in a temporary or persistent database ([see wiki](https://github.com/axllent/mailpit/wiki/Email-storage))
|
||||
- Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size, easily handling tens of thousands of emails
|
||||
- SMTP relaying / message release - relay messages via a different SMTP server ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-relay))
|
||||
- SMTP relaying / message release - relay messages via a different SMTP server including an optional allowlist of accepted recipients ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-relay))
|
||||
- Optional SMTP with STARTTLS & SMTP authentication, including an "accept anything" mode ([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))
|
||||
@@ -72,20 +72,14 @@ See [Docker instructions](https://github.com/axllent/mailpit/wiki/Docker-images)
|
||||
To build Mailpit from source see [building from source](https://github.com/axllent/mailpit/wiki/Building-from-source).
|
||||
|
||||
|
||||
### Testing Mailpit
|
||||
|
||||
Please refer to [the documentation](https://github.com/axllent/mailpit/wiki/Testing-Mailpit) of how to easily test email delivery to Mailpit.
|
||||
|
||||
|
||||
### Configuring sendmail
|
||||
|
||||
There are several different options available:
|
||||
|
||||
You can use `mailpit sendmail` as your sendmail configuration in `php.ini`:
|
||||
```
|
||||
sendmail_path = /usr/local/bin/mailpit sendmail
|
||||
```
|
||||
|
||||
If Mailpit is found on the same host as sendmail, you can symlink the Mailpit binary to sendmail, eg: `ln -s /usr/local/bin/mailpit /usr/sbin/sendmail` (only if Mailpit is running on default 1025 port).
|
||||
|
||||
You can use your default system `sendmail` binary to route directly to port `1025` (configurable) by calling `/usr/sbin/sendmail -S localhost:1025`.
|
||||
|
||||
You can build a Mailpit-specific sendmail binary from source (see [building from source](https://github.com/axllent/mailpit/wiki/Building-from-source)).
|
||||
Mailpit's SMTP server (by default on port 1025), so you will likely need to configure your sending application to deliver mail via that port. A common MTA (Mail Transfer Agent) that delivers system emails to a SMTP server is `sendmail`, used by many applications including PHP. Mailpit can also act as substitute for sendmail. For instructions of how to set this up, please refer to the [sendmail documentation](https://github.com/axllent/mailpit/wiki/Configuring-sendmail).
|
||||
|
||||
|
||||
## Why rewrite MailHog?
|
||||
|
||||
@@ -87,6 +87,7 @@ func init() {
|
||||
rootCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
|
||||
rootCmd.Flags().StringVar(&server.AccessControlAllowOrigin, "api-cors", server.AccessControlAllowOrigin, "Set API CORS Access-Control-Allow-Origin header")
|
||||
rootCmd.Flags().BoolVar(&config.UseMessageDates, "use-message-dates", config.UseMessageDates, "Use message dates as the received dates")
|
||||
rootCmd.Flags().BoolVar(&config.IgnoreDuplicateIDs, "ignore-duplicate-ids", config.IgnoreDuplicateIDs, "Ignore duplicate messages (by Message-Id)")
|
||||
|
||||
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI authentication")
|
||||
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key")
|
||||
@@ -97,11 +98,11 @@ func init() {
|
||||
rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-tls-cert", config.SMTPTLSCert, "TLS certificate for SMTP (STARTTLS) - requires smtp-tls-key")
|
||||
rootCmd.Flags().StringVar(&config.SMTPTLSKey, "smtp-tls-key", config.SMTPTLSKey, "TLS key for SMTP (STARTTLS) - requires smtp-tls-cert")
|
||||
rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", config.SMTPAuthAllowInsecure, "Enable insecure PLAIN & LOGIN authentication")
|
||||
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters")
|
||||
|
||||
rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages")
|
||||
rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)")
|
||||
|
||||
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters")
|
||||
rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
|
||||
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
|
||||
|
||||
@@ -201,8 +202,8 @@ func initConfigFromEnv() {
|
||||
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
|
||||
config.UseMessageDates = true
|
||||
}
|
||||
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
|
||||
config.UseMessageDates = true
|
||||
if getEnabledFromEnv("MP_IGNORE_DUPLICATE_IDS") {
|
||||
config.IgnoreDuplicateIDs = true
|
||||
}
|
||||
if getEnabledFromEnv("MP_QUIET") {
|
||||
logger.QuietLogging = true
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
sendmail "github.com/axllent/mailpit/sendmail/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
smtpAddr = "localhost:1025"
|
||||
fromAddr string
|
||||
)
|
||||
|
||||
// sendmailCmd represents the sendmail command
|
||||
var sendmailCmd = &cobra.Command{
|
||||
Use: "sendmail [flags] [recipients]",
|
||||
Short: "A sendmail command replacement for Mailpit",
|
||||
Long: `A sendmail command replacement for Mailpit.
|
||||
|
||||
You can optionally create a symlink called 'sendmail' to the Mailpit binary.`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
|
||||
sendmail.Run()
|
||||
},
|
||||
}
|
||||
@@ -25,13 +20,17 @@ You can optionally create a symlink called 'sendmail' to the Mailpit binary.`,
|
||||
func init() {
|
||||
rootCmd.AddCommand(sendmailCmd)
|
||||
|
||||
// these are simply repeated for cli consistency
|
||||
sendmailCmd.Flags().StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender")
|
||||
sendmailCmd.Flags().StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
|
||||
sendmailCmd.Flags().BoolVarP(&sendmail.Verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
|
||||
sendmailCmd.Flags().BoolP("long-b", "b", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-i", "i", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-o", "o", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-s", "s", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-t", "t", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
// print out manual help screen
|
||||
sendmailCmd.SetHelpTemplate(sendmail.HelpTemplate([]string{os.Args[0], "sendmail"}))
|
||||
|
||||
// these are simply repeated for cli consistency as cobra/viper does not allow
|
||||
// multi-letter single-dash variables (-bs)
|
||||
sendmailCmd.Flags().StringVarP(&sendmail.FromAddr, "from", "f", sendmail.FromAddr, "SMTP sender")
|
||||
sendmailCmd.Flags().StringVarP(&sendmail.SMTPAddr, "smtp-addr", "S", sendmail.SMTPAddr, "SMTP server address")
|
||||
sendmailCmd.Flags().BoolVarP(&sendmail.UseB, "long-b", "b", false, "Handle SMTP commands on standard input (use as -bs)")
|
||||
sendmailCmd.Flags().BoolVarP(&sendmail.UseS, "long-s", "s", false, "Handle SMTP commands on standard input (use as -bs)")
|
||||
sendmailCmd.Flags().BoolP("verbose", "v", false, "Verbose mode (sends debug output to stderr)")
|
||||
sendmailCmd.Flags().BoolP("long-i", "i", false, "Ignored")
|
||||
sendmailCmd.Flags().BoolP("long-o", "o", false, "Ignored")
|
||||
sendmailCmd.Flags().BoolP("long-t", "t", false, "Ignored")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/axllent/mailpit/utils/tools"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/tg123/go-htpasswd"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -41,7 +42,7 @@ var (
|
||||
// UIAuthFile for basic authentication
|
||||
UIAuthFile string
|
||||
|
||||
// UIAuth used for euthentication
|
||||
// UIAuth used for authentication
|
||||
UIAuth *htpasswd.File
|
||||
|
||||
// Webroot to define the base path for the UI and API
|
||||
@@ -65,11 +66,14 @@ var (
|
||||
// SMTPAuthAcceptAny accepts any username/password including none
|
||||
SMTPAuthAcceptAny bool
|
||||
|
||||
// IgnoreDuplicateIDs will skip messages with the same ID
|
||||
IgnoreDuplicateIDs bool
|
||||
|
||||
// SMTPCLITags is used to map the CLI args
|
||||
SMTPCLITags string
|
||||
|
||||
// TagRegexp is the allowed tag characters
|
||||
TagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`)
|
||||
// ValidTagRegexp represents a valid tag
|
||||
ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`)
|
||||
|
||||
// SMTPTags are expressions to apply tags to new mail
|
||||
SMTPTags []AutoTag
|
||||
@@ -83,7 +87,7 @@ var (
|
||||
// ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile
|
||||
ReleaseEnabled = false
|
||||
|
||||
// SMTPRelayAllIncoming is whether to relay all incoming messages via preconfgured SMTP server.
|
||||
// SMTPRelayAllIncoming is whether to relay all incoming messages via pre-configured SMTP server.
|
||||
// Use with extreme caution!
|
||||
SMTPRelayAllIncoming = false
|
||||
|
||||
@@ -108,15 +112,17 @@ type AutoTag struct {
|
||||
|
||||
// SMTPRelayConfigStruct struct for parsing yaml & storing variables
|
||||
type smtpRelayConfigStruct struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
STARTTLS bool `yaml:"starttls"`
|
||||
AllowInsecure bool `yaml:"allow-insecure"`
|
||||
Auth string `yaml:"auth"` // none, plain, cram-md5
|
||||
Username string `yaml:"username"` // plain & cram-md5
|
||||
Password string `yaml:"password"` // plain
|
||||
Secret string `yaml:"secret"` // cram-md5
|
||||
ReturnPath string `yaml:"return-path"` // allows overriding the boune address
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
STARTTLS bool `yaml:"starttls"`
|
||||
AllowInsecure bool `yaml:"allow-insecure"`
|
||||
Auth string `yaml:"auth"` // none, plain, login, cram-md5
|
||||
Username string `yaml:"username"` // plain & cram-md5
|
||||
Password string `yaml:"password"` // plain
|
||||
Secret string `yaml:"secret"` // cram-md5
|
||||
ReturnPath string `yaml:"return-path"` // allow overriding the bounce address
|
||||
RecipientAllowlist string `yaml:"recipient-allowlist"` // regex, if set needs to match for mails to be relayed
|
||||
RecipientAllowlistRegexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// VerifyConfig wil do some basic checking
|
||||
@@ -214,8 +220,8 @@ func VerifyConfig() error {
|
||||
for _, a := range args {
|
||||
t := strings.Split(a, "=")
|
||||
if len(t) > 1 {
|
||||
tag := strings.TrimSpace(t[0])
|
||||
if !TagRegexp.MatchString(tag) || len(tag) == 0 {
|
||||
tag := tools.CleanTag(t[0])
|
||||
if !ValidTagRegexp.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:], "=")))
|
||||
@@ -280,6 +286,11 @@ func parseRelayConfig(c string) error {
|
||||
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
|
||||
return fmt.Errorf("SMTP relay host username or password not set for PLAIN authentication (%s)", c)
|
||||
}
|
||||
} else if SMTPRelayConfig.Auth == "login" {
|
||||
SMTPRelayConfig.Auth = "login"
|
||||
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
|
||||
return fmt.Errorf("SMTP relay host username or password not set for LOGIN authentication (%s)", c)
|
||||
}
|
||||
} else if strings.HasPrefix(SMTPRelayConfig.Auth, "cram") {
|
||||
SMTPRelayConfig.Auth = "cram-md5"
|
||||
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Secret == "" {
|
||||
@@ -293,6 +304,18 @@ func parseRelayConfig(c string) error {
|
||||
|
||||
logger.Log().Infof("[smtp] enabling message relaying via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
|
||||
|
||||
allowlistRegexp, err := regexp.Compile(SMTPRelayConfig.RecipientAllowlist)
|
||||
|
||||
if SMTPRelayConfig.RecipientAllowlist != "" {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile recipient allowlist regexp: %e", err)
|
||||
}
|
||||
|
||||
SMTPRelayConfig.RecipientAllowlistRegexp = allowlistRegexp
|
||||
logger.Log().Infof("[smtp] recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.RecipientAllowlist)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Returns a JSON summary of the message and attachments.
|
||||
```json
|
||||
{
|
||||
"ID": "d7a5543b-96dd-478b-9b60-2b465c9884de",
|
||||
"MessageID": "12345.67890@localhost",
|
||||
"Read": true,
|
||||
"From": {
|
||||
"Name": "John Doe",
|
||||
@@ -31,6 +32,7 @@ Returns a JSON summary of the message and attachments.
|
||||
"ReplyTo": [],
|
||||
"Subject": "Message subject",
|
||||
"Date": "2016-09-07T16:46:00+13:00",
|
||||
"Tags": ["test"],
|
||||
"Text": "Plain text MIME part of the email",
|
||||
"HTML": "HTML MIME part (if exists)",
|
||||
"Size": 79499,
|
||||
|
||||
@@ -31,9 +31,11 @@ List messages in the mailbox. Messages are returned in the order of latest recei
|
||||
"unread": 500,
|
||||
"count": 50,
|
||||
"start": 0,
|
||||
"tags": ["test"],
|
||||
"messages": [
|
||||
{
|
||||
"ID": "1c575821-70ba-466f-8cee-2e1cf0fcdd0f",
|
||||
"MessageID": "12345.67890@localhost",
|
||||
"Read": false,
|
||||
"From": {
|
||||
"Name": "John Doe",
|
||||
@@ -54,6 +56,7 @@ List messages in the mailbox. Messages are returned in the order of latest recei
|
||||
"Bcc": [],
|
||||
"Subject": "Message subject",
|
||||
"Created": "2022-10-03T21:35:32.228605299+13:00",
|
||||
"Tags": ["test"],
|
||||
"Size": 6144,
|
||||
"Attachments": 0
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ Matching messages are returned in the order of latest received to oldest.
|
||||
"messages": [
|
||||
{
|
||||
"ID": "1c575821-70ba-466f-8cee-2e1cf0fcdd0f",
|
||||
"MessageID": "12345.67890@localhost",
|
||||
"Read": false,
|
||||
"From": {
|
||||
"Name": "John Doe",
|
||||
|
||||
@@ -14,6 +14,10 @@ const ctx = await esbuild.context(
|
||||
bundle: true,
|
||||
minify: doMinify,
|
||||
sourcemap: false,
|
||||
define: {
|
||||
'__VUE_OPTIONS_API__': 'true',
|
||||
'__VUE_PROD_DEVTOOLS__': 'false',
|
||||
},
|
||||
outdir: "server/ui/dist/",
|
||||
plugins: [pluginVue(), sassPlugin()],
|
||||
loader: {
|
||||
|
||||
38
go.mod
38
go.mod
@@ -8,20 +8,21 @@ require (
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jhillyerd/enmime v0.11.1
|
||||
github.com/k3a/html2text v1.1.0
|
||||
github.com/klauspost/compress v1.16.5
|
||||
github.com/jhillyerd/enmime v1.0.0
|
||||
github.com/k3a/html2text v1.2.1
|
||||
github.com/klauspost/compress v1.16.6
|
||||
github.com/leporo/sqlf v1.4.0
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/mhale/smtpd v0.8.0
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/tg123/go-htpasswd v1.2.1
|
||||
golang.org/x/text v0.9.0
|
||||
golang.org/x/text v0.10.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.21.2
|
||||
modernc.org/sqlite v1.23.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -36,27 +37,28 @@ require (
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/reiver/go-oi v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/image v0.7.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.10.0 // indirect
|
||||
golang.org/x/image v0.8.0 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/net v0.11.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/tools v0.10.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.4 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/cc/v3 v3.41.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.14 // indirect
|
||||
modernc.org/libc v1.24.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.6.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
|
||||
83
go.sum
83
go.sum
@@ -57,16 +57,16 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jhillyerd/enmime v0.11.1 h1:U6ToGVxfxNQQhKrAaGxtwOf7Zqksb8AQ3j1CyAWOk5k=
|
||||
github.com/jhillyerd/enmime v0.11.1/go.mod h1:EktNOa/V6ka9yCrfoB2uxgefp1lno6OVdszW0iQ5LnM=
|
||||
github.com/jhillyerd/enmime v1.0.0 h1:8swYgO1fm68PllCKz5jiLzgD3axNUS388jr6BtRSsl8=
|
||||
github.com/jhillyerd/enmime v1.0.0/go.mod h1:EktNOa/V6ka9yCrfoB2uxgefp1lno6OVdszW0iQ5LnM=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k3a/html2text v1.1.0 h1:ks4hKSTdiTRsLr0DM771mI5TvsoG6zH7m1Ulv7eJRHw=
|
||||
github.com/k3a/html2text v1.1.0/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
|
||||
github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY=
|
||||
github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
@@ -76,8 +76,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c=
|
||||
github.com/leporo/sqlf v1.4.0/go.mod h1:pgN9yKsAnQ+2ewhbZogr98RcasUjPsHF3oXwPPhHvBw=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
@@ -93,7 +93,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
|
||||
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -104,8 +107,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
@@ -131,26 +134,26 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
|
||||
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
|
||||
golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg=
|
||||
golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM=
|
||||
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.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -159,8 +162,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -168,15 +171,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
|
||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -188,25 +191,25 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
||||
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
||||
modernc.org/ccgo/v3 v3.16.14 h1:af6KNtFgsVmnDYrWk3PQCS9XT6BXe7o3ZFJKkIKvXNQ=
|
||||
modernc.org/ccgo/v3 v3.16.14/go.mod h1:mPDSujUIaTNWQSG4eqKw+atqLOEbma6Ncsa94WbC9zo=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/libc v1.22.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ=
|
||||
modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
|
||||
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
||||
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.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
|
||||
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.21.2 h1:ixuUG0QS413Vfzyx6FWx6PYTmHaOegTY+hjzhn7L+a0=
|
||||
modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
|
||||
1401
package-lock.json
generated
1401
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
||||
"axios": "^1.2.1",
|
||||
"bootstrap": "^5.2.0",
|
||||
"bootstrap-icons": "^1.9.1",
|
||||
"bootstrap5-tags": "^1.4.41",
|
||||
"bootstrap5-tags": "^1.6.1",
|
||||
"color-hash": "^2.0.2",
|
||||
"moment": "^2.29.4",
|
||||
"prismjs": "^1.29.0",
|
||||
"rapidoc": "^9.3.4",
|
||||
@@ -21,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@vue/compiler-sfc": "^3.2.37",
|
||||
"esbuild": "^0.17.5",
|
||||
"esbuild": "^0.18.10",
|
||||
"esbuild-plugin-vue-next": "^0.1.4",
|
||||
"esbuild-sass-plugin": "^2.3.2"
|
||||
}
|
||||
|
||||
@@ -3,8 +3,16 @@ package cmd
|
||||
|
||||
/**
|
||||
* Bare bones sendmail drop-in replacement borrowed from MailHog
|
||||
*
|
||||
* It uses a bit of a hack for flag parsing in order to be compatible
|
||||
* with the cobra sendmail subcommand, as sendmail uses `-bc` which
|
||||
* is not POSIX compatible.
|
||||
*
|
||||
* The -bs command-line switch causes sendmail to run a single SMTP session in the
|
||||
* foreground over its standard input and output, and then exit. The SMTP session
|
||||
* is exactly like a network SMTP session. Usually, one or more messages are
|
||||
* submitted to sendmail for delivery.
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
@@ -13,21 +21,27 @@ import (
|
||||
"net/smtp"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/reiver/go-telnet"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
// Verbose flag
|
||||
Verbose bool
|
||||
// SMTPAddr address
|
||||
SMTPAddr = "localhost:1025"
|
||||
// FromAddr email address
|
||||
FromAddr string
|
||||
|
||||
fromAddr string
|
||||
// UseB - used to set from `-bs`
|
||||
UseB bool
|
||||
// UseS - used to set from `-bs`
|
||||
UseS bool
|
||||
)
|
||||
|
||||
// Run the Mailpit sendmail replacement.
|
||||
func Run() {
|
||||
func init() {
|
||||
host, err := os.Hostname()
|
||||
if err != nil {
|
||||
host = "localhost"
|
||||
@@ -39,47 +53,68 @@ func Run() {
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
if fromAddr == "" {
|
||||
fromAddr = username + "@" + host
|
||||
if FromAddr == "" {
|
||||
FromAddr = username + "@" + host
|
||||
}
|
||||
}
|
||||
|
||||
smtpAddr := "localhost:1025"
|
||||
var recip []string
|
||||
// Run the Mailpit sendmail replacement.
|
||||
func Run() {
|
||||
var recipients []string
|
||||
|
||||
// defaults from envars if provided
|
||||
if len(os.Getenv("MP_SENDMAIL_SMTP_ADDR")) > 0 {
|
||||
smtpAddr = os.Getenv("MP_SENDMAIL_SMTP_ADDR")
|
||||
SMTPAddr = os.Getenv("MP_SENDMAIL_SMTP_ADDR")
|
||||
}
|
||||
if len(os.Getenv("MP_SENDMAIL_FROM")) > 0 {
|
||||
fromAddr = os.Getenv("MP_SENDMAIL_FROM")
|
||||
FromAddr = os.Getenv("MP_SENDMAIL_FROM")
|
||||
}
|
||||
|
||||
// override defaults from cli flags
|
||||
flag.StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender address")
|
||||
flag.StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
|
||||
flag.BoolVarP(&Verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
|
||||
flag.BoolP("long-b", "b", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-i", "i", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-o", "o", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-s", "s", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-t", "t", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.CommandLine.SortFlags = false
|
||||
flag.StringVarP(&FromAddr, "from", "f", FromAddr, "SMTP sender")
|
||||
flag.StringVarP(&SMTPAddr, "smtp-addr", "S", SMTPAddr, "SMTP server address")
|
||||
flag.BoolVarP(&UseB, "long-b", "b", false, "Handle SMTP commands on standard input (use as -bs)")
|
||||
flag.BoolVarP(&UseS, "long-s", "s", false, "Handle SMTP commands on standard input (use as -bs)")
|
||||
flag.BoolP("verbose", "v", false, "Ignored")
|
||||
flag.BoolP("long-i", "i", false, "Ignored")
|
||||
flag.BoolP("long-o", "o", false, "Ignored")
|
||||
flag.BoolP("long-t", "t", false, "Ignored")
|
||||
|
||||
// set the default help
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("A sendmail command replacement for Mailpit (%s).\n\n", config.Version)
|
||||
fmt.Printf("Usage:\n %s [flags] [recipients]\n", os.Args[0])
|
||||
fmt.Println("\nFlags:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Println(HelpTemplate(os.Args[0:1]))
|
||||
}
|
||||
|
||||
var showHelp bool
|
||||
// avoid 'pflag: help requested' error
|
||||
flag.BoolVarP(&showHelp, "help", "h", false, "")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// allow recipient to be passed as an argument
|
||||
recip = flag.Args()
|
||||
// allow recipients to be passed as an argument
|
||||
recipients = flag.Args()
|
||||
|
||||
if Verbose {
|
||||
fmt.Fprintln(os.Stdout, smtpAddr, fromAddr)
|
||||
if showHelp {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// ensure -bs is set
|
||||
if UseB && !UseS || !UseB && UseS {
|
||||
fmt.Printf("error: use -bs")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// handles `sendmail -bs`
|
||||
if UseB && UseS {
|
||||
var caller telnet.Caller = telnet.StandardCaller
|
||||
|
||||
// telnet directly to SMTP
|
||||
if err := telnet.DialToAndCall(SMTPAddr, caller); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(os.Stdin)
|
||||
@@ -96,8 +131,8 @@ func Run() {
|
||||
|
||||
addresses := []string{}
|
||||
|
||||
if len(recip) > 0 {
|
||||
addresses = recip
|
||||
if len(recipients) > 0 {
|
||||
addresses = recipients
|
||||
} else {
|
||||
// get all recipients in To, Cc and Bcc
|
||||
if to, err := msg.Header.AddressList("To"); err == nil {
|
||||
@@ -117,9 +152,28 @@ func Run() {
|
||||
}
|
||||
}
|
||||
|
||||
err = smtp.SendMail(smtpAddr, nil, fromAddr, addresses, body)
|
||||
err = smtp.SendMail(SMTPAddr, nil, FromAddr, addresses, body)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error sending mail")
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// HelpTemplate returns a string of the help
|
||||
func HelpTemplate(args []string) string {
|
||||
return fmt.Sprintf(`A sendmail command replacement for Mailpit (%s)
|
||||
|
||||
Usage: %s [flags] [recipients] < message
|
||||
|
||||
See: https://github.com/axllent/mailpit
|
||||
|
||||
Flags:
|
||||
-S string SMTP server address (default "localhost:1025")
|
||||
-f string Set the envelope sender address (default "%s")
|
||||
-bs Handle SMTP commands on standard input
|
||||
-t Ignored
|
||||
-i Ignored
|
||||
-o Ignored
|
||||
-v Ignored
|
||||
`, config.Version, strings.Join(args, " "), FromAddr)
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@ func GetMessages(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: start
|
||||
// in: query
|
||||
// description: pagination offset
|
||||
// description: Pagination offset
|
||||
// required: false
|
||||
// type: integer
|
||||
// default: 0
|
||||
// + name: limit
|
||||
// in: query
|
||||
// description: limit results
|
||||
// description: Limit results
|
||||
// required: false
|
||||
// type: integer
|
||||
// default: 50
|
||||
@@ -88,12 +88,12 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: query
|
||||
// in: query
|
||||
// description: search query
|
||||
// description: Search query
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: limit
|
||||
// in: query
|
||||
// description: limit results
|
||||
// description: Limit results
|
||||
// required: false
|
||||
// type: integer
|
||||
// default: 50
|
||||
@@ -119,7 +119,7 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var res MessagesSummary
|
||||
|
||||
res.Start = 0
|
||||
res.Start = start
|
||||
res.Messages = messages
|
||||
res.Count = len(messages)
|
||||
res.Total = stats.Total
|
||||
@@ -147,7 +147,7 @@ func GetMessage(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// description: Database ID
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
@@ -188,12 +188,12 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// description: Database ID
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: PartID
|
||||
// in: path
|
||||
// description: attachment part id
|
||||
// description: Attachment part ID
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
@@ -237,7 +237,7 @@ func GetHeaders(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// description: Database ID
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
@@ -284,7 +284,7 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// description: Database ID
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
@@ -304,7 +304,7 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
if dl == "1" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
|
||||
}
|
||||
@@ -330,7 +330,7 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ids
|
||||
// in: body
|
||||
// description: Message ids to delete
|
||||
// description: Database IDs to delete
|
||||
// required: false
|
||||
// type: DeleteRequest
|
||||
//
|
||||
@@ -381,7 +381,7 @@ func SetReadStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ids
|
||||
// in: body
|
||||
// description: Message ids to update
|
||||
// description: Database IDs to update
|
||||
// required: false
|
||||
// type: SetReadStatusRequest
|
||||
//
|
||||
@@ -459,7 +459,7 @@ func SetTags(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ids
|
||||
// in: body
|
||||
// description: Message ids to update
|
||||
// description: Database IDs to update
|
||||
// required: true
|
||||
// type: SetTagsRequest
|
||||
//
|
||||
@@ -495,14 +495,14 @@ func SetTags(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// ReleaseMessage (method: POST) will release a message via a preconfigured external SMTP server.
|
||||
// ReleaseMessage (method: POST) will release a message via a pre-configured external SMTP server.
|
||||
// If no IDs are provided then all messages are updated.
|
||||
func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route POST /api/v1/message/{ID}/release message Release
|
||||
//
|
||||
// # Release message
|
||||
//
|
||||
// Release a message via a preconfigured external SMTP server..
|
||||
// Release a message via a pre-configured external SMTP server..
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
@@ -515,10 +515,10 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// description: Database ID
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: To
|
||||
// + name: to
|
||||
// in: body
|
||||
// description: Array of email addresses to release message to
|
||||
// required: true
|
||||
@@ -554,10 +554,17 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
for _, to := range tos {
|
||||
if _, err := mail.ParseAddress(to); err != nil {
|
||||
address, err := mail.ParseAddress(to)
|
||||
|
||||
if err != nil {
|
||||
httpError(w, "Invalid email address: "+to)
|
||||
return
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.RecipientAllowlistRegexp != nil && !config.SMTPRelayConfig.RecipientAllowlistRegexp.MatchString(address.Address) {
|
||||
httpError(w, "Mail address does not match allowlist: "+to)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(msg)
|
||||
@@ -650,3 +657,10 @@ func getStartLimit(req *http.Request) (start int, limit int) {
|
||||
|
||||
return start, limit
|
||||
}
|
||||
|
||||
// GetOptions returns a blank response
|
||||
func GetOptions(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte(""))
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ func Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// description: Database ID
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: PartID
|
||||
// in: path
|
||||
// description: attachment part id
|
||||
// description: Attachment part ID
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
|
||||
@@ -20,6 +20,8 @@ type webUIConfiguration struct {
|
||||
SMTPServer string
|
||||
// Enforced Return-Path (if set) for relay bounces
|
||||
ReturnPath string
|
||||
// Allowlist of accepted recipients
|
||||
RecipientAllowlist string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +47,7 @@ func WebUIConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if config.ReleaseEnabled {
|
||||
conf.MessageRelay.SMTPServer = fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath
|
||||
conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.RecipientAllowlist
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(conf)
|
||||
|
||||
@@ -68,10 +68,10 @@ func Listen() {
|
||||
isReady.Store(true)
|
||||
|
||||
if config.UITLSCert != "" && config.UITLSKey != "" {
|
||||
logger.Log().Infof("[http] starting secure server on https://%s%s", logger.CleanIP(config.HTTPListen), config.Webroot)
|
||||
logger.Log().Infof("[http] starting secure server on https://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
|
||||
logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UITLSCert, config.UITLSKey, nil))
|
||||
} else {
|
||||
logger.Log().Infof("[http] starting server on http://%s%s", logger.CleanIP(config.HTTPListen), config.Webroot)
|
||||
logger.Log().Infof("[http] starting server on http://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
|
||||
logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil))
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,9 @@ func defaultRoutes() *mux.Router {
|
||||
r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
|
||||
r.HandleFunc(config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig)).Methods("GET")
|
||||
|
||||
// return blank 200 response for OPTIONS requests for CORS
|
||||
r.PathPrefix(config.Webroot + "api/v1/").Handler(middleWareFunc(apiv1.GetOptions)).Methods("OPTIONS")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -122,6 +125,8 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
if AccessControlAllowOrigin != "" && strings.HasPrefix(r.RequestURI, config.Webroot+"api/") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", AccessControlAllowOrigin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
}
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
@@ -159,6 +164,8 @@ func middlewareHandler(h http.Handler) http.Handler {
|
||||
|
||||
if AccessControlAllowOrigin != "" && strings.HasPrefix(r.RequestURI, config.Webroot+"api/") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", AccessControlAllowOrigin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
}
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
|
||||
@@ -2,19 +2,53 @@ package smtpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
)
|
||||
|
||||
func allowedRecipients(to []string) []string {
|
||||
if config.SMTPRelayConfig.RecipientAllowlistRegexp == nil {
|
||||
return to
|
||||
}
|
||||
|
||||
var ar []string
|
||||
|
||||
for _, recipient := range to {
|
||||
address, err := mail.ParseAddress(recipient)
|
||||
|
||||
if err != nil {
|
||||
logger.Log().Warnf("ignoring invalid email address: %s", recipient)
|
||||
continue
|
||||
}
|
||||
|
||||
if !config.SMTPRelayConfig.RecipientAllowlistRegexp.MatchString(address.Address) {
|
||||
logger.Log().Debugf("[smtp] not allowed to relay to %s: does not match the allowlist %s", recipient, config.SMTPRelayConfig.RecipientAllowlist)
|
||||
} else {
|
||||
ar = append(ar, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
return ar
|
||||
}
|
||||
|
||||
// Send will connect to a pre-configured SMTP server and send a message to one or more recipients.
|
||||
func Send(from string, to []string, msg []byte) error {
|
||||
recipients := allowedRecipients(to)
|
||||
|
||||
if len(recipients) == 0 {
|
||||
return errors.New("no valid recipients")
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error connecting to %s: %s", addr, err.Error())
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
@@ -25,7 +59,7 @@ func Send(from string, to []string, msg []byte) error {
|
||||
conf.InsecureSkipVerify = config.SMTPRelayConfig.AllowInsecure
|
||||
|
||||
if err = c.StartTLS(conf); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error creating StartTLS config: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,37 +69,71 @@ func Send(from string, to []string, msg []byte) error {
|
||||
a = smtp.PlainAuth("", config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password, config.SMTPRelayConfig.Host)
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "login" {
|
||||
a = LoginAuth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password)
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "cram-md5" {
|
||||
a = smtp.CRAMMD5Auth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Secret)
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error response to AUTH command: %s", err.Error())
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error response to MAIL command: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
for _, addr := range recipients {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error response to DATA command: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error sending message: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error closing connection: %s", err.Error())
|
||||
}
|
||||
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Custom implementation of LOGIN SMTP authentication
|
||||
// @see https://gist.github.com/andelf/5118732
|
||||
type loginAuth struct {
|
||||
username, password string
|
||||
}
|
||||
|
||||
// LoginAuth authentication
|
||||
func LoginAuth(username, password string) smtp.Auth {
|
||||
return &loginAuth{username, password}
|
||||
}
|
||||
|
||||
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
return "LOGIN", []byte{}, nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
switch string(fromServer) {
|
||||
case "Username:":
|
||||
return []byte(a.username), nil
|
||||
case "Password:":
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, errors.New("Unknown fromServer")
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -24,18 +24,25 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
messageID := strings.Trim(msg.Header.Get("Message-Id"), "<>")
|
||||
|
||||
// add a message ID if not set
|
||||
if msg.Header.Get("Message-Id") == "" {
|
||||
if messageID == "" {
|
||||
// generate unique ID
|
||||
uid := uuid.NewV4().String() + "@mailpit"
|
||||
messageID = uuid.NewV4().String() + "@mailpit"
|
||||
// add unique ID
|
||||
data = append([]byte("Message-Id: <"+uid+">\r\n"), data...)
|
||||
data = append([]byte("Message-Id: <"+messageID+">\r\n"), data...)
|
||||
} else if config.IgnoreDuplicateIDs {
|
||||
if storage.MessageIDExists(messageID) {
|
||||
logger.Log().Debugf("[smtpd] duplicate message found, ignoring %s", messageID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if enabled, this will route the email 1:1 through to the preconfigured smtp server
|
||||
if config.SMTPRelayAllIncoming {
|
||||
if err := Send(from, to, data); err != nil {
|
||||
logger.Log().Errorf("[smtp] error relaying message: %s", err.Error())
|
||||
logger.Log().Warnf("[smtp] error relaying message: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[smtp] relayed message from %s via %s:%d", from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
}
|
||||
@@ -81,8 +88,9 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
||||
logger.Log().Debugf("[smtpd] added missing addresses to Bcc header: %s", strings.Join(missingAddresses, ", "))
|
||||
}
|
||||
|
||||
if _, err := storage.Store(data); err != nil {
|
||||
logger.Log().Errorf("[db] error storing message: %d", err.Error())
|
||||
_, err = storage.Store(data)
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] error storing message: %s", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import "./assets/styles.scss";
|
||||
import "../../node_modules/bootstrap-icons/font/bootstrap-icons.scss";
|
||||
import "bootstrap-icons/font/bootstrap-icons.scss";
|
||||
import "bootstrap";
|
||||
|
||||
createApp(App).mount('#app');
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// Removed "Noto Color Emoji" from list re: https://github.com/axllent/mailpit/issues/92
|
||||
$font-family-sans-serif: 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";
|
||||
|
||||
$link-decoration: none;
|
||||
$primary: #2c3e50;
|
||||
$list-group-disabled-color: #adb5bd;
|
||||
$enable-negative-margins: true;
|
||||
$body-color-dark: #e7eaed;
|
||||
|
||||
1
server/ui-src/assets/bootstrap.scss
vendored
1
server/ui-src/assets/bootstrap.scss
vendored
@@ -4,6 +4,7 @@
|
||||
// Configuration
|
||||
@import "../../../node_modules/bootstrap/scss/functions";
|
||||
@import "../../../node_modules/bootstrap/scss/variables";
|
||||
@import "../../../node_modules/bootstrap/scss/variables-dark";
|
||||
@import "../../../node_modules/bootstrap/scss/maps";
|
||||
@import "../../../node_modules/bootstrap/scss/mixins";
|
||||
@import "../../../node_modules/bootstrap/scss/utilities";
|
||||
|
||||
@@ -56,8 +56,17 @@
|
||||
z-index: 1500;
|
||||
}
|
||||
|
||||
.message.read:not(.active):not(.selected) {
|
||||
color: $gray-500;
|
||||
.message {
|
||||
&.read {
|
||||
color: $text-muted;
|
||||
|
||||
b {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
background: var(--bs-primary-bg-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
#nav-plain-text .text-view,
|
||||
@@ -86,7 +95,9 @@
|
||||
}
|
||||
|
||||
#nav-html {
|
||||
padding-right: 1.5rem;
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
#preview-html {
|
||||
@@ -180,20 +191,6 @@
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.message.selected {
|
||||
background: $gray-300;
|
||||
|
||||
.text-muted {
|
||||
color: $body-color !important;
|
||||
}
|
||||
|
||||
&.read {
|
||||
b {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.blur {
|
||||
.privacy {
|
||||
filter: blur(3px);
|
||||
@@ -280,8 +277,8 @@ body.blur {
|
||||
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css */
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #000;
|
||||
background: 0 0;
|
||||
// color: #000;
|
||||
// background: 0 0;
|
||||
font-size: 0.85em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
@@ -314,7 +311,7 @@ code[class*="language-"] {
|
||||
}
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background-color: #fdfdfd;
|
||||
// background-color: #fdfdfd;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
@@ -364,7 +361,7 @@ pre[class*="language-"] {
|
||||
.token.url,
|
||||
.token.variable {
|
||||
color: #a67f59;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
// background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
@@ -379,7 +376,7 @@ pre[class*="language-"] {
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #a67f59;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
// background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.token.important {
|
||||
font-weight: 400;
|
||||
@@ -390,9 +387,9 @@ pre[class*="language-"] {
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
// .token.entity {
|
||||
// cursor: help;
|
||||
// }
|
||||
.token.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import axios from 'axios';
|
||||
import { Modal } from 'bootstrap';
|
||||
import moment from 'moment';
|
||||
import axios from 'axios'
|
||||
import { Modal } from 'bootstrap'
|
||||
import moment from 'moment'
|
||||
import ColorHash from 'color-hash'
|
||||
|
||||
|
||||
// FakeModal is used to return a fake Bootstrap modal
|
||||
// if the ID returns nothing
|
||||
// if the ID returns nothing to prevent errors.
|
||||
function FakeModal() { }
|
||||
FakeModal.prototype.hide = function () { alert('close fake modal') }
|
||||
FakeModal.prototype.show = function () { alert('open fake modal') }
|
||||
FakeModal.prototype.hide = function () { }
|
||||
FakeModal.prototype.show = function () { }
|
||||
|
||||
// Set up the color hash generator lightness and hue to ensure darker colors
|
||||
const colorHash = new ColorHash({ lightness: 0.3, saturation: [0.35, 0.5, 0.65] });
|
||||
|
||||
/* Common mixin functions used in apps */
|
||||
const commonMixins = {
|
||||
data() {
|
||||
return {
|
||||
loading: 0
|
||||
loading: 0,
|
||||
tagColorCache: {},
|
||||
showTagColors: true
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.showTagColors = localStorage.getItem('showTagsColors')
|
||||
},
|
||||
|
||||
methods: {
|
||||
getFileSize: function (bytes) {
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
@@ -201,6 +211,27 @@ const commonMixins = {
|
||||
}
|
||||
|
||||
return 'bi-file-arrow-down-fill';
|
||||
},
|
||||
|
||||
// Returns a hex color based on a string.
|
||||
// Values are stored in an array for faster lookup / processing.
|
||||
colorHash: function (s) {
|
||||
if (this.tagColorCache[s] != undefined) {
|
||||
return this.tagColorCache[s]
|
||||
}
|
||||
this.tagColorCache[s] = colorHash.hex(s)
|
||||
|
||||
return this.tagColorCache[s]
|
||||
},
|
||||
|
||||
toggleTagColors: function () {
|
||||
if (this.showTagColors) {
|
||||
localStorage.removeItem('showTagsColors')
|
||||
this.showTagColors = false
|
||||
} else {
|
||||
localStorage.setItem('showTagsColors', '1')
|
||||
this.showTagColors = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,18 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="mt-4 border-top pt-4">
|
||||
<a v-for="part in attachments" :href="'api/v1/message/'+message.ID+'/part/'+part.PartID" class="card attachment float-start me-3 mb-3" target="_blank" style="width: 180px">
|
||||
<img v-if="isImage(part)" :src="'api/v1/message/'+message.ID+'/part/'+part.PartID+'/thumb'" class="card-img-top" alt="">
|
||||
<img v-else src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAB4AQMAAABhKUq+AAAAA1BMVEX///+nxBvIAAAAGUlEQVQYGe3BgQAAAADDoPtTT+EA1QAAgFsLQAAB12s2WgAAAABJRU5ErkJggg==" class="card-img-top" alt="">
|
||||
<a v-for="part in attachments" :href="'api/v1/message/' + message.ID + '/part/' + part.PartID"
|
||||
class="card attachment float-start me-3 mb-3" target="_blank" style="width: 180px">
|
||||
<img v-if="isImage(part)" :src="'api/v1/message/' + message.ID + '/part/' + part.PartID + '/thumb'"
|
||||
class="card-img-top" alt="">
|
||||
<img v-else
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAB4AQMAAABhKUq+AAAAA1BMVEX///+nxBvIAAAAGUlEQVQYGe3BgQAAAADDoPtTT+EA1QAAgFsLQAAB12s2WgAAAABJRU5ErkJggg=="
|
||||
class="card-img-top" alt="">
|
||||
<div class="icon" v-if="!isImage(part)">
|
||||
<i class="bi" :class="attachmentIcon(part)"></i>
|
||||
</div>
|
||||
<div class="card-body border-0">
|
||||
<p class="mb-1 text-muted">
|
||||
<p class="mb-1">
|
||||
<i class="bi me-1" :class="attachmentIcon(part)"></i>
|
||||
<small>{{ getFileSize(part.Size) }}</small>
|
||||
</p>
|
||||
@@ -29,7 +33,7 @@ export default {
|
||||
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer small border-0 text-center text-truncate">
|
||||
<div class="card-footer small border-0 text-center text-truncate">
|
||||
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<script>
|
||||
import commonMixins from '../mixins.js';
|
||||
import commonMixins from '../mixins.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -17,9 +17,9 @@ export default {
|
||||
|
||||
mounted() {
|
||||
let self = this;
|
||||
let uri = 'api/v1/message/' + self.message.ID + '/headers';
|
||||
let uri = 'api/v1/message/' + self.message.ID + '/headers'
|
||||
self.get(uri, false, function (response) {
|
||||
self.headers = response.data;
|
||||
self.headers = response.data
|
||||
});
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
<div v-if="headers" class="small">
|
||||
<div v-for="vals, k in headers" class="row mb-2 pb-2 border-bottom w-100">
|
||||
<div class="col-md-4 col-lg-3 col-xl-2 mb-2"><b>{{ k }}</b></div>
|
||||
<div class="col-md-8 col-lg-9 col-xl-10 text-muted">
|
||||
<div class="col-md-8 col-lg-9 col-xl-10 text-body-secondary">
|
||||
<div v-for="x in vals" class="mb-2 text-break">{{ x }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
<script>
|
||||
import commonMixins from '../mixins.js';
|
||||
import Prism from "prismjs";
|
||||
import Tags from "bootstrap5-tags";
|
||||
import Attachments from './Attachments.vue';
|
||||
import Headers from './Headers.vue';
|
||||
import commonMixins from '../mixins.js'
|
||||
import Prism from "prismjs"
|
||||
import Tags from "bootstrap5-tags"
|
||||
import Attachments from './Attachments.vue'
|
||||
import Headers from './Headers.vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
|
||||
components: {
|
||||
Attachments,
|
||||
Headers
|
||||
Headers,
|
||||
},
|
||||
|
||||
mixins: [commonMixins],
|
||||
@@ -41,20 +41,20 @@ export default {
|
||||
watch: {
|
||||
message: {
|
||||
handler() {
|
||||
let self = this;
|
||||
self.showTags = false;
|
||||
self.messageTags = self.message.Tags;
|
||||
self.allTags = self.existingTags;
|
||||
self.loadHeaders = false;
|
||||
let self = this
|
||||
self.showTags = false
|
||||
self.messageTags = self.message.Tags
|
||||
self.allTags = self.existingTags
|
||||
self.loadHeaders = false
|
||||
self.scaleHTMLPreview = 'display';// default view
|
||||
// delay to select first tab and add HTML highlighting (prev/next)
|
||||
self.$nextTick(function () {
|
||||
self.renderUI();
|
||||
self.showTags = true;
|
||||
self.renderUI()
|
||||
self.showTags = true
|
||||
self.$nextTick(function () {
|
||||
Tags.init("select[multiple]");
|
||||
});
|
||||
});
|
||||
Tags.init("select[multiple]")
|
||||
})
|
||||
})
|
||||
},
|
||||
// force eager callback execution
|
||||
immediate: true
|
||||
@@ -62,97 +62,112 @@ export default {
|
||||
messageTags() {
|
||||
// save changed to tags
|
||||
if (this.showTags) {
|
||||
this.saveTags();
|
||||
this.saveTags()
|
||||
}
|
||||
},
|
||||
scaleHTMLPreview() {
|
||||
if (this.scaleHTMLPreview == 'display') {
|
||||
let self = this;
|
||||
let self = this
|
||||
window.setTimeout(function () {
|
||||
self.resizeIframes();
|
||||
}, 500);
|
||||
self.resizeIframes()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let self = this;
|
||||
self.showTags = false;
|
||||
self.allTags = self.existingTags;
|
||||
window.addEventListener("resize", self.resizeIframes);
|
||||
self.renderUI();
|
||||
let self = this
|
||||
self.showTags = false
|
||||
self.allTags = self.existingTags
|
||||
window.addEventListener("resize", self.resizeIframes)
|
||||
self.renderUI()
|
||||
|
||||
let headersTab = document.getElementById('nav-headers-tab');
|
||||
let headersTab = document.getElementById('nav-headers-tab')
|
||||
headersTab.addEventListener('shown.bs.tab', function (event) {
|
||||
self.loadHeaders = true;
|
||||
});
|
||||
self.loadHeaders = true
|
||||
})
|
||||
|
||||
let rawTab = document.getElementById('nav-raw-tab');
|
||||
let rawTab = document.getElementById('nav-raw-tab')
|
||||
rawTab.addEventListener('shown.bs.tab', function (event) {
|
||||
self.srcURI = 'api/v1/message/' + self.message.ID + '/raw';
|
||||
self.resizeIframes();
|
||||
});
|
||||
self.srcURI = 'api/v1/message/' + self.message.ID + '/raw'
|
||||
self.resizeIframes()
|
||||
})
|
||||
|
||||
self.showTags = true;
|
||||
self.showTags = true
|
||||
self.$nextTick(function () {
|
||||
Tags.init("select[multiple]");
|
||||
});
|
||||
Tags.init("select[multiple]")
|
||||
})
|
||||
},
|
||||
|
||||
unmounted: function () {
|
||||
window.removeEventListener("resize", this.resizeIframes);
|
||||
window.removeEventListener("resize", this.resizeIframes)
|
||||
},
|
||||
|
||||
methods: {
|
||||
renderUI: function () {
|
||||
let self = this;
|
||||
let self = this
|
||||
// click the first non-disabled tab
|
||||
document.querySelector('#nav-tab button:not([disabled])').click();
|
||||
document.activeElement.blur(); // blur focus
|
||||
document.getElementById('message-view').scrollTop = 0;
|
||||
document.querySelector('#nav-tab button:not([disabled])').click()
|
||||
document.activeElement.blur() // blur focus
|
||||
document.getElementById('message-view').scrollTop = 0
|
||||
|
||||
// delay 0.2s until vue has rendered the iframe content
|
||||
window.setTimeout(function () {
|
||||
let p = document.getElementById('preview-html');
|
||||
let p = document.getElementById('preview-html')
|
||||
if (p) {
|
||||
// make links open in new window
|
||||
let anchorEls = p.contentWindow.document.body.querySelectorAll('a');
|
||||
let anchorEls = p.contentWindow.document.body.querySelectorAll('a')
|
||||
for (var i = 0; i < anchorEls.length; i++) {
|
||||
let anchorEl = anchorEls[i];
|
||||
let href = anchorEl.getAttribute('href');
|
||||
let anchorEl = anchorEls[i]
|
||||
let href = anchorEl.getAttribute('href')
|
||||
|
||||
if (href && href.match(/^http/)) {
|
||||
anchorEl.setAttribute('target', '_blank');
|
||||
anchorEl.setAttribute('target', '_blank')
|
||||
}
|
||||
}
|
||||
self.resizeIframes();
|
||||
self.resizeIframes()
|
||||
}
|
||||
}, 200);
|
||||
}, 200)
|
||||
|
||||
// html highlighting
|
||||
window.Prism = window.Prism || {};
|
||||
window.Prism.manual = true;
|
||||
Prism.highlightAll();
|
||||
window.Prism = window.Prism || {}
|
||||
window.Prism.manual = true
|
||||
Prism.highlightAll()
|
||||
},
|
||||
|
||||
resizeIframe: function (el) {
|
||||
let i = el.target;
|
||||
i.style.height = i.contentWindow.document.body.scrollHeight + 50 + 'px';
|
||||
let i = el.target
|
||||
i.style.height = i.contentWindow.document.body.scrollHeight + 50 + 'px'
|
||||
},
|
||||
|
||||
resizeIframes: function () {
|
||||
if (this.scaleHTMLPreview != 'display') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
let h = document.getElementById('preview-html');
|
||||
let h = document.getElementById('preview-html')
|
||||
if (h) {
|
||||
h.style.height = h.contentWindow.document.body.scrollHeight + 50 + 'px';
|
||||
h.style.height = h.contentWindow.document.body.scrollHeight + 50 + 'px'
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
// set the iframe body & text colors based on current theme
|
||||
initRawIframe: function (el) {
|
||||
let bodyStyles = window.getComputedStyle(document.body, null)
|
||||
let bg = bodyStyles.getPropertyValue('background-color')
|
||||
let txt = bodyStyles.getPropertyValue('color')
|
||||
|
||||
let body = el.target.contentWindow.document.querySelector('body')
|
||||
if (body) {
|
||||
body.style.color = txt
|
||||
body.style.backgroundColor = bg
|
||||
}
|
||||
|
||||
this.resizeIframe(el)
|
||||
},
|
||||
|
||||
saveTags: function () {
|
||||
let self = this;
|
||||
let self = this
|
||||
|
||||
var data = {
|
||||
ids: [this.message.ID],
|
||||
@@ -160,16 +175,42 @@ export default {
|
||||
}
|
||||
|
||||
self.put('api/v1/tags', data, function (response) {
|
||||
self.scrollInPlace = true;
|
||||
self.$emit('loadMessages');
|
||||
});
|
||||
self.scrollInPlace = true
|
||||
self.$emit('loadMessages')
|
||||
})
|
||||
},
|
||||
|
||||
// Convert plain text to HTML including anchor links
|
||||
textToHTML: function (s) {
|
||||
let html = s
|
||||
|
||||
// full links with http(s)
|
||||
let re = /(\b(https?|ftp):\/\/[\-\w@:%_\+.~#?,&\/\/=;]+)\b/gim
|
||||
html = html.replace(re, '˱˱˱a href=ˠˠˠ$&ˠˠˠ target=_blank rel=noopener˲˲˲$&˱˱˱/a˲˲˲')
|
||||
|
||||
// plain www links without https?:// prefix
|
||||
let re2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim
|
||||
html = html.replace(re2, '$1˱˱˱a href=ˠˠˠhttp://$2ˠˠˠ target=ˠˠˠ_blankˠˠˠ rel=ˠˠˠnoopenerˠˠˠ˲˲˲$2˱˱˱/a˲˲˲')
|
||||
|
||||
// escape to HTML & convert <>" back
|
||||
html = html
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/˱˱˱/g, '<')
|
||||
.replace(/˲˲˲/g, '>')
|
||||
.replace(/ˠˠˠ/g, '"')
|
||||
|
||||
return html
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="message" id="message-view" class="mh-100" style="overflow-y: scroll;">
|
||||
<div v-if="message" id="message-view" class="px-2 px-md-0 mh-100" style="overflow-y: scroll;">
|
||||
<div class="row w-100">
|
||||
<div class="col-md">
|
||||
<table class="messageHeaders">
|
||||
@@ -195,7 +236,7 @@ export default {
|
||||
<template v-if="i > 0">, </template>
|
||||
<span class="text-nowrap">{{ t.Name + " <" + t.Address + ">" }}</span>
|
||||
</span>
|
||||
<span v-else>Undisclosed recipients</span>
|
||||
<span v-else class="text-body-secondary">[Undisclosed recipients]</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.Cc && message.Cc.length" class="small">
|
||||
@@ -217,7 +258,7 @@ export default {
|
||||
</tr>
|
||||
<tr v-if="message.ReplyTo && message.ReplyTo.length" class="small">
|
||||
<th class="text-nowrap">Reply-To</th>
|
||||
<td class="privacy text-muted">
|
||||
<td class="privacy text-body-secondary">
|
||||
<span v-for="(t, i) in message.ReplyTo">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address + ">" }} </span>
|
||||
@@ -225,13 +266,13 @@ export default {
|
||||
</tr>
|
||||
<tr v-if="message.ReturnPath && message.ReturnPath != message.From.Address" class="small">
|
||||
<th class="text-nowrap">Return-Path</th>
|
||||
<td class="privacy text-muted"><{{ message.ReturnPath }}></td>
|
||||
<td class="privacy text-body-secondary"><{{ message.ReturnPath }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="small">Subject</th>
|
||||
<td>
|
||||
<strong v-if="message.Subject != ''">{{ message.Subject }}</strong>
|
||||
<small class="text-muted" v-else>[ no subject ]</small>
|
||||
<small class="text-body-secondary" v-else>[ no subject ]</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="d-md-none small">
|
||||
@@ -243,9 +284,10 @@ export default {
|
||||
<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="|,|">
|
||||
data-full-width="false" data-suggestions-threshold="1" 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>
|
||||
@@ -273,26 +315,25 @@ export default {
|
||||
v-on:click="showMobileBtns = true; resizeIframes()">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"
|
||||
v-on:click="showMobileBtns = false">
|
||||
v-on:click=" showMobileBtns = false">
|
||||
HTML <span class="d-sm-none">Src</span><span class="d-none d-sm-inline">Source</span>
|
||||
</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' : ''" v-on:click="showMobileBtns = false">Text</button>
|
||||
:class="message.HTML == '' ? 'show' : ''" v-on:click=" showMobileBtns = false">Text</button>
|
||||
<button class="nav-link" id="nav-headers-tab" data-bs-toggle="tab" data-bs-target="#nav-headers"
|
||||
type="button" role="tab" aria-controls="nav-headers" aria-selected="false"
|
||||
v-on:click="showMobileBtns = false">
|
||||
v-on:click=" showMobileBtns = false">
|
||||
<span class="d-sm-none">Hdrs</span><span class="d-none d-sm-inline">Headers</span>
|
||||
</button>
|
||||
<button class="nav-link" id="nav-raw-tab" data-bs-toggle="tab" data-bs-target="#nav-raw" type="button"
|
||||
role="tab" aria-controls="nav-raw" aria-selected="false"
|
||||
v-on:click="showMobileBtns = false">Raw</button>
|
||||
v-on:click=" showMobileBtns = false">Raw</button>
|
||||
|
||||
<div class="d-none d-lg-block ms-auto me-2" v-if="showMobileBtns">
|
||||
<template v-for="vals, key in responsiveSizes">
|
||||
<button class="btn" :class="scaleHTMLPreview == key ? 'btn-outline-primary' : ''"
|
||||
:disabled="scaleHTMLPreview == key" :title="'Switch to ' + key + ' view'"
|
||||
v-on:click="scaleHTMLPreview = key">
|
||||
<template v-for=" vals, key in responsiveSizes ">
|
||||
<button class="btn" :disabled="scaleHTMLPreview == key" :title="'Switch to ' + key + ' view'"
|
||||
v-on:click=" scaleHTMLPreview = key">
|
||||
<i class="bi" :class="'bi-' + key"></i>
|
||||
</button>
|
||||
</template>
|
||||
@@ -305,7 +346,7 @@ export default {
|
||||
aria-labelledby="nav-html-tab" tabindex="0">
|
||||
<div id="responsive-view" :class="scaleHTMLPreview" :style="responsiveSizes[scaleHTMLPreview]">
|
||||
<iframe target-blank="" class="tab-pane d-block" id="preview-html" :srcdoc="message.HTML"
|
||||
v-on:load="resizeIframe" seamless frameborder="0" style="width: 100%; height: 100%;">
|
||||
v-on:load="resizeIframe" frameborder="0" style="width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
</div>
|
||||
<Attachments v-if="allAttachments(message).length" :message="message"
|
||||
@@ -317,7 +358,7 @@ export default {
|
||||
</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="text-view">{{ message.Text }}</div>
|
||||
<div class="text-view" v-html="textToHTML(message.Text)"></div>
|
||||
<Attachments v-if="allAttachments(message).length" :message="message"
|
||||
:attachments="allAttachments(message)"></Attachments>
|
||||
</div>
|
||||
@@ -325,8 +366,8 @@ export default {
|
||||
<Headers v-if="loadHeaders" :message="message"></Headers>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-raw" role="tabpanel" aria-labelledby="nav-raw-tab" tabindex="0">
|
||||
<iframe v-if="srcURI" :src="srcURI" v-on:load="resizeIframe" seamless frameborder="0"
|
||||
style="width: 100%; height: 300px;" id="message-src"></iframe>
|
||||
<iframe v-if="srcURI" :src="srcURI" v-on:load="initRawIframe" frameborder="0"
|
||||
style="width: 100%; height: 300px"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
<script>
|
||||
import Tags from "bootstrap5-tags";
|
||||
import commonMixins from '../mixins.js';
|
||||
import Tags from "bootstrap5-tags"
|
||||
import commonMixins from '../mixins.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -19,19 +19,19 @@ export default {
|
||||
mixins: [commonMixins],
|
||||
|
||||
mounted() {
|
||||
this.addresses = JSON.parse(JSON.stringify(this.releaseAddresses));
|
||||
this.addresses = JSON.parse(JSON.stringify(this.releaseAddresses))
|
||||
this.$nextTick(function () {
|
||||
Tags.init("select[multiple]");
|
||||
});
|
||||
Tags.init("select[multiple]")
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
releaseMessage: function () {
|
||||
let self = this;
|
||||
let self = this
|
||||
// set timeout to allow for user clicking send before the tag filter has applied the tag
|
||||
window.setTimeout(function () {
|
||||
if (!self.addresses.length) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
let data = {
|
||||
@@ -39,9 +39,9 @@ export default {
|
||||
}
|
||||
|
||||
self.post('api/v1/message/' + self.message.ID + '/release', data, function (response) {
|
||||
self.modal("ReleaseModal").hide();
|
||||
});
|
||||
}, 100);
|
||||
self.modal("ReleaseModal").hide()
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,19 +57,21 @@ export default {
|
||||
<div class="modal-body">
|
||||
<h6>Send this message to one or more addresses specified below.</h6>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label text-muted">From</label>
|
||||
<label class="col-sm-2 col-form-label text-body-secondary">From</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" :value="message.From.Address">
|
||||
<input type="text" aria-label="From address" readonly class="form-control-plaintext"
|
||||
:value="message.From.Address">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class=" col-sm-2 col-form-label text-muted">Subject</label>
|
||||
<label class=" col-sm-2 col-form-label text-body-secondary">Subject</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" :value="message.Subject">
|
||||
<input type="text" aria-label="Subject" readonly class="form-control-plaintext"
|
||||
:value="message.Subject">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label text-muted">Send to</label>
|
||||
<label class="col-sm-2 col-form-label text-body-secondary">Send to</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select tag-selector" v-model="addresses" multiple data-allow-new="true"
|
||||
data-clear-end="true" data-allow-clear="true" data-placeholder="Enter email addresses..."
|
||||
@@ -83,6 +85,11 @@ export default {
|
||||
<div class="invalid-feedback">Invalid email address</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text text-center" v-if="relayConfig.MessageRelay.RecipientAllowlist != ''">
|
||||
Note: A recipient allowlist has been configured. Any mail address not matching it will be rejected.
|
||||
<br class="d-none d-md-inline">
|
||||
Configured allowlist: <b>{{ relayConfig.MessageRelay.RecipientAllowlist }}</b>
|
||||
</div>
|
||||
<div class="form-text text-center">
|
||||
Note: For testing purposes, a unique Message-Id will be generated on send.
|
||||
<br class="d-none d-md-inline">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import commonMixins from '../mixins.js';
|
||||
import commonMixins from '../mixins.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -12,7 +12,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="card mt-4">
|
||||
<div class="card-body text-muted small">
|
||||
<div class="card-body text-body-secondary small">
|
||||
<p class="card-text">
|
||||
<b>Message date:</b><br>
|
||||
<small>{{ messageDate(message.Date) }}</small>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { Toast } from 'bootstrap';
|
||||
import { Toast } from 'bootstrap'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -7,15 +7,15 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let self = this;
|
||||
let el = document.getElementById('messageToast');
|
||||
let self = this
|
||||
let el = document.getElementById('messageToast')
|
||||
if (el) {
|
||||
el.addEventListener('hidden.bs.toast', () => {
|
||||
self.$emit("clearMessageToast");
|
||||
self.$emit("clearMessageToast")
|
||||
})
|
||||
|
||||
let b = Toast.getOrCreateInstance(el);
|
||||
b.show();
|
||||
let b = Toast.getOrCreateInstance(el)
|
||||
b.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
|
||||
<div class="toast-body">
|
||||
<div>
|
||||
<a :href="'#' + message.ID" class="d-block text-truncate text-muted">
|
||||
<a :href="'#' + message.ID" class="d-block text-truncate text-body-secondary">
|
||||
<template v-if="message.Subject != ''">{{ message.Subject }}</template>
|
||||
<template v-else>[ no subject ]</template>
|
||||
</a>
|
||||
|
||||
123
server/ui-src/templates/ThemeToggle.vue
Normal file
123
server/ui-src/templates/ThemeToggle.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
theme: 'auto',
|
||||
icon: '#circle-half',
|
||||
icons: {
|
||||
'auto': '#circle-half',
|
||||
'light': '#sun-fill',
|
||||
'dark': '#moon-stars-fill'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.setTheme(this.getPreferredTheme())
|
||||
},
|
||||
|
||||
methods: {
|
||||
getStoredTheme: function () {
|
||||
let theme = localStorage.getItem('theme')
|
||||
if (!theme) {
|
||||
theme = 'auto'
|
||||
}
|
||||
|
||||
return theme
|
||||
},
|
||||
|
||||
setStoredTheme: function (theme) {
|
||||
localStorage.setItem('theme', theme)
|
||||
this.setTheme(theme)
|
||||
},
|
||||
|
||||
getPreferredTheme: function () {
|
||||
const storedTheme = this.getStoredTheme()
|
||||
if (storedTheme) {
|
||||
return storedTheme
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
},
|
||||
|
||||
setTheme: function (theme) {
|
||||
this.icon = this.icons[theme]
|
||||
this.theme = theme
|
||||
if (
|
||||
theme === 'auto' &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark')
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-bs-theme', theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="bootstrap" viewBox="0 0 512 408" fill="currentcolor">
|
||||
<path
|
||||
d="M106.342 0c-29.214 0-50.827 25.58-49.86 53.32.927 26.647-.278 61.165-8.966 89.31C38.802 170.862 24.07 188.707 0 191v26c24.069 2.293 38.802 20.138 47.516 48.37 8.688 28.145 9.893 62.663 8.965 89.311C55.515 382.42 77.128 408 106.342 408h299.353c29.214 0 50.827-25.58 49.861-53.319-.928-26.648.277-61.166 8.964-89.311 8.715-28.232 23.411-46.077 47.48-48.37v-26c-24.069-2.293-38.765-20.138-47.48-48.37-8.687-28.145-9.892-62.663-8.964-89.31C456.522 25.58 434.909 0 405.695 0H106.342zm236.559 251.102c0 38.197-28.501 61.355-75.798 61.355h-87.202a2 2 0 01-2-2v-213a2 2 0 012-2h86.74c39.439 0 65.322 21.354 65.322 54.138 0 23.008-17.409 43.61-39.594 47.219v1.203c30.196 3.309 50.532 24.212 50.532 53.085zm-84.58-128.125h-45.91v64.814h38.669c29.888 0 46.373-12.03 46.373-33.535 0-20.151-14.174-31.279-39.132-31.279zm-45.91 90.53v71.431h47.605c31.12 0 47.605-12.482 47.605-35.941 0-23.46-16.947-35.49-49.608-35.49h-45.602z" />
|
||||
</symbol>
|
||||
<symbol id="check2" viewBox="0 0 16 16" fill="currentcolor">
|
||||
<path
|
||||
d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||
</symbol>
|
||||
<symbol id="circle-half" viewBox="0 0 16 16" fill="currentcolor">
|
||||
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
|
||||
</symbol>
|
||||
<symbol id="moon-stars-fill" viewBox="0 0 16 16" fill="currentcolor">
|
||||
<path
|
||||
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
|
||||
<path
|
||||
d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
|
||||
</symbol>
|
||||
<symbol id="sun-fill" viewBox="0 0 16 16" fill="currentcolor">
|
||||
<path
|
||||
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
<div class="dropdown bd-mode-toggle float-end me-2 d-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" aria-expanded="false"
|
||||
title="Toggle theme" data-bs-toggle="dropdown" aria-label="Toggle theme">
|
||||
<svg class="bi my-1 theme-icon-active" width="1em" height="1em">
|
||||
<use :href="icon"></use>
|
||||
</svg>
|
||||
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center"
|
||||
:class="theme == 'light' ? 'active' : ''" @click="setStoredTheme('light')">
|
||||
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
|
||||
<use href="#sun-fill"></use>
|
||||
</svg>
|
||||
Light
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center"
|
||||
:class="theme == 'dark' ? 'active' : ''" @click="setStoredTheme('dark')">
|
||||
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
|
||||
<use href="#moon-stars-fill"></use>
|
||||
</svg>
|
||||
Dark
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center"
|
||||
:class="theme == 'auto' ? 'active' : ''" @click="setStoredTheme('auto')">
|
||||
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
|
||||
<use href="#circle-half"></use>
|
||||
</svg>
|
||||
Auto
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
@@ -66,7 +66,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"description": "Database ID",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -103,7 +103,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"description": "Database ID",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -142,14 +142,14 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"description": "Database ID",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "attachment part id",
|
||||
"description": "Attachment part ID",
|
||||
"name": "PartID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -183,14 +183,14 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"description": "Database ID",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "attachment part id",
|
||||
"description": "Attachment part ID",
|
||||
"name": "PartID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -224,7 +224,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"description": "Database ID",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -242,7 +242,7 @@
|
||||
},
|
||||
"/api/v1/message/{ID}/release": {
|
||||
"post": {
|
||||
"description": "Release a message via a preconfigured external SMTP server..",
|
||||
"description": "Release a message via a pre-configured external SMTP server..",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -261,14 +261,14 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"description": "Database ID",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Array of email addresses to release message to",
|
||||
"name": "To",
|
||||
"name": "to",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
@@ -307,14 +307,14 @@
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"description": "pagination offset",
|
||||
"description": "Pagination offset",
|
||||
"name": "start",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
"description": "limit results",
|
||||
"description": "Limit results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
@@ -347,11 +347,11 @@
|
||||
"operationId": "SetReadStatus",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Message ids to update",
|
||||
"description": "Database IDs to update",
|
||||
"name": "ids",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"description": "Message ids to update",
|
||||
"description": "Database IDs to update",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/SetReadStatusRequest"
|
||||
}
|
||||
@@ -385,11 +385,11 @@
|
||||
"operationId": "Delete",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Message ids to delete",
|
||||
"description": "Database IDs to delete",
|
||||
"name": "ids",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"description": "Message ids to delete",
|
||||
"description": "Database IDs to delete",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/DeleteRequest"
|
||||
}
|
||||
@@ -423,7 +423,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "search query",
|
||||
"description": "Search query",
|
||||
"name": "query",
|
||||
"in": "query",
|
||||
"required": true
|
||||
@@ -431,7 +431,7 @@
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
"description": "limit results",
|
||||
"description": "Limit results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
@@ -466,12 +466,12 @@
|
||||
"operationId": "SetTags",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Message ids to update",
|
||||
"description": "Database IDs to update",
|
||||
"name": "ids",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"description": "Message ids to update",
|
||||
"description": "Database IDs to update",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/SetTagsRequest"
|
||||
}
|
||||
@@ -568,23 +568,23 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ContentID": {
|
||||
"description": "content id",
|
||||
"description": "Content ID",
|
||||
"type": "string"
|
||||
},
|
||||
"ContentType": {
|
||||
"description": "content type",
|
||||
"description": "Content type",
|
||||
"type": "string"
|
||||
},
|
||||
"FileName": {
|
||||
"description": "file name",
|
||||
"description": "File name",
|
||||
"type": "string"
|
||||
},
|
||||
"PartID": {
|
||||
"description": "attachment part id",
|
||||
"description": "Attachment part ID",
|
||||
"type": "string"
|
||||
},
|
||||
"Size": {
|
||||
"description": "size in bytes",
|
||||
"description": "Size in bytes",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
@@ -645,7 +645,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ID": {
|
||||
"description": "Unique message database id",
|
||||
"description": "Database ID",
|
||||
"type": "string"
|
||||
},
|
||||
"Inline": {
|
||||
@@ -655,6 +655,10 @@
|
||||
"$ref": "#/definitions/Attachment"
|
||||
}
|
||||
},
|
||||
"MessageID": {
|
||||
"description": "Message ID",
|
||||
"type": "string"
|
||||
},
|
||||
"Read": {
|
||||
"description": "Read status",
|
||||
"type": "boolean"
|
||||
@@ -667,7 +671,7 @@
|
||||
}
|
||||
},
|
||||
"ReturnPath": {
|
||||
"description": "ReturnPath is the Return-Path",
|
||||
"description": "Return-Path",
|
||||
"type": "string"
|
||||
},
|
||||
"Size": {
|
||||
@@ -744,7 +748,11 @@
|
||||
"$ref": "#/definitions/Address"
|
||||
},
|
||||
"ID": {
|
||||
"description": "Unique message database id",
|
||||
"description": "Database ID",
|
||||
"type": "string"
|
||||
},
|
||||
"MessageID": {
|
||||
"description": "Message ID",
|
||||
"type": "string"
|
||||
},
|
||||
"Read": {
|
||||
@@ -897,6 +905,10 @@
|
||||
"description": "Whether message relaying (release) is enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"RecipientAllowlist": {
|
||||
"description": "Allowlist of accepted recipients",
|
||||
"type": "string"
|
||||
},
|
||||
"ReturnPath": {
|
||||
"description": "Enforced Return-Path (if set) for relay bounces",
|
||||
"type": "string"
|
||||
|
||||
@@ -72,20 +72,51 @@ var (
|
||||
Script: `ALTER TABLE mailbox ADD COLUMN Tags Text NOT NULL DEFAULT '[]';
|
||||
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
|
||||
},
|
||||
{
|
||||
Version: 1.2,
|
||||
Description: "Creating new mailbox format",
|
||||
Script: `CREATE TABLE IF NOT EXISTS mailboxtmp (
|
||||
Created INTEGER NOT NULL,
|
||||
ID TEXT NOT NULL,
|
||||
MessageID TEXT NOT NULL,
|
||||
Subject TEXT NOT NULL,
|
||||
Metadata TEXT,
|
||||
Size INTEGER NOT NULL,
|
||||
Inline INTEGER NOT NULL,
|
||||
Attachments INTEGER NOT NULL,
|
||||
Read INTEGER,
|
||||
Tags TEXT,
|
||||
SearchText TEXT
|
||||
);
|
||||
INSERT INTO mailboxtmp
|
||||
(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Tags)
|
||||
SELECT
|
||||
Sort, ID, '', json_extract(Data, '$.Subject'),Data,
|
||||
json_extract(Data, '$.Size'), json_extract(Data, '$.Inline'), json_extract(Data, '$.Attachments'),
|
||||
Search, Read, Tags
|
||||
FROM mailbox;
|
||||
|
||||
DROP TABLE IF EXISTS mailbox;
|
||||
ALTER TABLE mailboxtmp RENAME TO mailbox;
|
||||
CREATE INDEX IF NOT EXISTS idx_created ON mailbox (Created);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_id ON mailbox (MessageID);
|
||||
CREATE INDEX IF NOT EXISTS idx_subject ON mailbox (Subject);
|
||||
CREATE INDEX IF NOT EXISTS idx_size ON mailbox (Size);
|
||||
CREATE INDEX IF NOT EXISTS idx_inline ON mailbox (Inline);
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments ON mailbox (Attachments);
|
||||
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
|
||||
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// DBMailSummary struct for storing mail summary
|
||||
type DBMailSummary struct {
|
||||
Created time.Time
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
Subject string
|
||||
Size int
|
||||
Inline int
|
||||
Attachments int
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
}
|
||||
|
||||
// InitDB will initialise the database
|
||||
@@ -144,6 +175,8 @@ func InitDB() error {
|
||||
// auto-prune & delete
|
||||
go dbCron()
|
||||
|
||||
go dataMigrations()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -174,7 +207,7 @@ func Close() {
|
||||
|
||||
// Store will save an email to the database tables
|
||||
func Store(body []byte) (string, error) {
|
||||
// Parse message body with enmime.
|
||||
// Parse message body with enmime
|
||||
env, err := enmime.ReadEnvelope(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
logger.Log().Warningf("[db] %s", err.Error())
|
||||
@@ -189,22 +222,21 @@ func Store(body []byte) (string, error) {
|
||||
from = &mail.Address{Name: env.GetHeader("From")}
|
||||
}
|
||||
|
||||
messageID := strings.Trim(env.Root.Header.Get("Message-ID"), "<>")
|
||||
|
||||
obj := DBMailSummary{
|
||||
Created: time.Now(),
|
||||
From: from,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Size: len(body),
|
||||
Inline: len(env.Inlines),
|
||||
Attachments: len(env.Attachments),
|
||||
From: from,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
}
|
||||
|
||||
created := time.Now()
|
||||
|
||||
// use message date instead of created date
|
||||
if config.UseMessageDates {
|
||||
if mDate, err := env.Date(); err == nil {
|
||||
obj.Created = mDate
|
||||
created = mDate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +251,16 @@ func Store(body []byte) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tagData := findTags(&body)
|
||||
// extract tags from body matches based on --tag
|
||||
tagStr := findTagsInRawMessage(&body)
|
||||
|
||||
// extract tags from X-Tags header
|
||||
headerTags := strings.TrimSpace(env.Root.Header.Get("X-Tags"))
|
||||
if headerTags != "" {
|
||||
tagStr += "," + headerTags
|
||||
}
|
||||
|
||||
tagData := uniqueTagsFromString(tagStr)
|
||||
|
||||
tagJSON, err := json.Marshal(tagData)
|
||||
if err != nil {
|
||||
@@ -237,8 +278,14 @@ func Store(body []byte) (string, error) {
|
||||
// roll back if it fails
|
||||
defer tx.Rollback()
|
||||
|
||||
subject := env.GetHeader("Subject")
|
||||
size := len(body)
|
||||
inline := len(env.Inlines)
|
||||
attachments := len(env.Attachments)
|
||||
|
||||
// insert mail summary data
|
||||
_, err = tx.Exec("INSERT INTO mailbox(ID, Data, Search, Tags, Read) values(?,?,?,?,0)", id, string(summaryJSON), searchText, string(tagJSON))
|
||||
_, err = tx.Exec("INSERT INTO mailbox(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Tags, Read) values(?,?,?,?,?,?,?,?,?,?,0)",
|
||||
created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, string(tagJSON))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -259,9 +306,13 @@ func Store(body []byte) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c.Tags = tagData
|
||||
|
||||
c.Created = created
|
||||
c.ID = id
|
||||
c.MessageID = messageID
|
||||
c.Attachments = attachments
|
||||
c.Subject = subject
|
||||
c.Size = size
|
||||
c.Tags = tagData
|
||||
|
||||
websockets.Broadcast("new", c)
|
||||
|
||||
@@ -276,24 +327,29 @@ func List(start, limit int) ([]MessageSummary, error) {
|
||||
results := []MessageSummary{}
|
||||
|
||||
q := sqlf.From("mailbox").
|
||||
Select(`ID, Data, Tags, Read`).
|
||||
OrderBy("Sort DESC").
|
||||
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags`).
|
||||
OrderBy("Created DESC").
|
||||
Limit(limit).
|
||||
Offset(start)
|
||||
|
||||
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
|
||||
var created int64
|
||||
var id string
|
||||
var summary string
|
||||
var messageID string
|
||||
var subject string
|
||||
var metadata string
|
||||
var size int
|
||||
var attachments int
|
||||
var tags string
|
||||
var read int
|
||||
em := MessageSummary{}
|
||||
|
||||
if err := row.Scan(&id, &summary, &tags, &read); err != nil {
|
||||
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(summary), &em); err != nil {
|
||||
if err := json.Unmarshal([]byte(metadata), &em); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
@@ -303,11 +359,18 @@ func List(start, limit int) ([]MessageSummary, error) {
|
||||
return
|
||||
}
|
||||
|
||||
em.Created = time.UnixMilli(created)
|
||||
em.ID = id
|
||||
em.MessageID = messageID
|
||||
em.Subject = subject
|
||||
em.Size = size
|
||||
em.Attachments = attachments
|
||||
em.Read = read == 1
|
||||
|
||||
results = append(results, em)
|
||||
|
||||
// logger.PrettyPrint(em)
|
||||
|
||||
}); err != nil {
|
||||
return results, err
|
||||
}
|
||||
@@ -318,7 +381,7 @@ func List(start, limit int) ([]MessageSummary, error) {
|
||||
}
|
||||
|
||||
// Search will search a mailbox for search terms.
|
||||
// The search is broken up by segments (exact phrases can be quoted), and interprits specific terms such as:
|
||||
// The search is broken up by segments (exact phrases can be quoted), and interprets 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, start, limit int) ([]MessageSummary, error) {
|
||||
@@ -342,19 +405,24 @@ func Search(search string, start, limit int) ([]MessageSummary, error) {
|
||||
q := searchParser(args, start, limit)
|
||||
|
||||
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
|
||||
var created int64
|
||||
var id string
|
||||
var summary string
|
||||
var messageID string
|
||||
var subject string
|
||||
var metadata string
|
||||
var size int
|
||||
var attachments int
|
||||
var tags string
|
||||
var read int
|
||||
var ignore string
|
||||
em := MessageSummary{}
|
||||
|
||||
if err := row.Scan(&id, &summary, &tags, &read, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore); err != nil {
|
||||
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &ignore, &ignore, &ignore, &ignore); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(summary), &em); err != nil {
|
||||
if err := json.Unmarshal([]byte(metadata), &em); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
@@ -364,7 +432,12 @@ func Search(search string, start, limit int) ([]MessageSummary, error) {
|
||||
return
|
||||
}
|
||||
|
||||
em.Created = time.UnixMilli(created)
|
||||
em.ID = id
|
||||
em.MessageID = messageID
|
||||
em.Subject = subject
|
||||
em.Size = size
|
||||
em.Attachments = attachments
|
||||
em.Read = read == 1
|
||||
|
||||
results = append(results, em)
|
||||
@@ -404,8 +477,10 @@ func GetMessage(id string) (*Message, error) {
|
||||
from = &mail.Address{Name: env.GetHeader("From")}
|
||||
}
|
||||
|
||||
messageID := strings.Trim(env.GetHeader("Message-ID"), "<>")
|
||||
|
||||
returnPath := strings.Trim(env.GetHeader("Return-Path"), "<>")
|
||||
if returnPath == "" {
|
||||
if returnPath == "" && from != nil {
|
||||
returnPath = from.Address
|
||||
}
|
||||
|
||||
@@ -413,27 +488,20 @@ func GetMessage(id string) (*Message, error) {
|
||||
if err != nil {
|
||||
// return received datetime when message does not contain a date header
|
||||
q := sqlf.From("mailbox").
|
||||
Select(`Data`).
|
||||
OrderBy("Sort DESC").
|
||||
Select(`Created`).
|
||||
Where(`ID = ?`, id)
|
||||
|
||||
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
|
||||
var summary string
|
||||
em := MessageSummary{}
|
||||
var created int64
|
||||
|
||||
if err := row.Scan(&summary); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(summary), &em); err != nil {
|
||||
if err := row.Scan(&created); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id)
|
||||
|
||||
date = em.Created
|
||||
date = time.UnixMilli(created)
|
||||
}); err != nil {
|
||||
logger.Log().Error(err)
|
||||
}
|
||||
@@ -441,6 +509,7 @@ func GetMessage(id string) (*Message, error) {
|
||||
|
||||
obj := Message{
|
||||
ID: id,
|
||||
MessageID: messageID,
|
||||
Read: true,
|
||||
From: from,
|
||||
Date: date,
|
||||
@@ -821,3 +890,16 @@ func IsUnread(id string) bool {
|
||||
|
||||
return unread == 1
|
||||
}
|
||||
|
||||
// MessageIDExists checks whether a Message-ID exists in the DB
|
||||
func MessageIDExists(id string) bool {
|
||||
var total int
|
||||
|
||||
q := sqlf.From("mailbox").
|
||||
Select("COUNT(*)").To(&total).
|
||||
Where("MessageID = ?", id)
|
||||
|
||||
_ = q.QueryRowAndClose(nil, db)
|
||||
|
||||
return total != 0
|
||||
}
|
||||
|
||||
200
storage/migrationTasks.go
Normal file
200
storage/migrationTasks.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/leporo/sqlf"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
func dataMigrations() {
|
||||
updateOrderByCreatedTask()
|
||||
assignMessageIDsTask()
|
||||
}
|
||||
|
||||
// Update Created column using Created metadata datetime <= v1.6.5
|
||||
// Migration task implemented 05/2023 - can be removed end 2023
|
||||
func updateOrderByCreatedTask() {
|
||||
q := sqlf.From("mailbox").
|
||||
Select("ID").
|
||||
Select(`json_extract(Metadata, '$.Created') as Created`).
|
||||
Where("Created < ?", 1155000600)
|
||||
|
||||
toUpdate := make(map[string]int64)
|
||||
p := message.NewPrinter(language.English)
|
||||
|
||||
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
|
||||
var id string
|
||||
var ts sql.NullString
|
||||
if err := row.Scan(&id, &ts); err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ts.Valid {
|
||||
logger.Log().Errorf("[migration] cannot get Created timestamp from %s", id)
|
||||
return
|
||||
}
|
||||
|
||||
t, _ := time.Parse(time.RFC3339Nano, ts.String)
|
||||
toUpdate[id] = t.UnixMilli()
|
||||
}); err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
return
|
||||
}
|
||||
|
||||
total := len(toUpdate)
|
||||
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Log().Infof("[migration] updating timestamp for %s messages", p.Sprintf("%d", len(toUpdate)))
|
||||
|
||||
// begin a transaction
|
||||
ctx := context.Background()
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
return
|
||||
}
|
||||
|
||||
// roll back if it fails
|
||||
defer tx.Rollback()
|
||||
|
||||
var blockTime = time.Now()
|
||||
|
||||
count := 0
|
||||
for id, ts := range toUpdate {
|
||||
count++
|
||||
_, err := tx.Exec(`UPDATE mailbox SET Created = ? WHERE ID = ?`, ts, id)
|
||||
if err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
}
|
||||
|
||||
if count%1000 == 0 {
|
||||
percent := (100 * count) / total
|
||||
logger.Log().Infof("[migration] updated timestamp for 1,000 messages [%d%%] in %s", percent, time.Since(blockTime))
|
||||
blockTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Infof("[migration] commit %s changes", p.Sprintf("%d", count))
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Log().Infof("[migration] complete")
|
||||
}
|
||||
|
||||
// Find any messages without a stored Message-ID and update it <= v1.6.5
|
||||
// Migration task implemented 05/2023 - can be removed end 2023
|
||||
func assignMessageIDsTask() {
|
||||
if !config.IgnoreDuplicateIDs {
|
||||
return
|
||||
}
|
||||
|
||||
q := sqlf.From("mailbox").
|
||||
Select("ID").
|
||||
Where("MessageID = ''")
|
||||
|
||||
missingIDS := make(map[string]string)
|
||||
|
||||
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
|
||||
var id string
|
||||
if err := row.Scan(&id); err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
return
|
||||
}
|
||||
missingIDS[id] = ""
|
||||
}); err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
}
|
||||
|
||||
if len(missingIDS) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var count int
|
||||
var blockTime = time.Now()
|
||||
p := message.NewPrinter(language.English)
|
||||
|
||||
total := len(missingIDS)
|
||||
|
||||
logger.Log().Infof("[migration] extracting Message-IDs for %s messages", p.Sprintf("%d", total))
|
||||
|
||||
for id := range missingIDS {
|
||||
raw, err := GetMessageRaw(id)
|
||||
if err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
continue
|
||||
}
|
||||
|
||||
r := bytes.NewReader(raw)
|
||||
|
||||
env, err := enmime.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
continue
|
||||
}
|
||||
|
||||
messageID := strings.Trim(env.GetHeader("Message-ID"), "<>")
|
||||
|
||||
missingIDS[id] = messageID
|
||||
|
||||
count++
|
||||
|
||||
if count%1000 == 0 {
|
||||
percent := (100 * count) / total
|
||||
logger.Log().Infof("[migration] extracted 1,000 Message-IDs [%d%%] in %s", percent, time.Since(blockTime))
|
||||
blockTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// begin a transaction
|
||||
ctx := context.Background()
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
return
|
||||
}
|
||||
|
||||
// roll back if it fails
|
||||
defer tx.Rollback()
|
||||
|
||||
count = 0
|
||||
|
||||
for id, mid := range missingIDS {
|
||||
_, err = tx.Exec(`UPDATE mailbox SET MessageID = ? WHERE ID = ?`, mid, id)
|
||||
if err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
if count%1000 == 0 {
|
||||
percent := (100 * count) / total
|
||||
logger.Log().Infof("[migration] stored 1,000 Message-IDs [%d%%] in %s", percent, time.Since(blockTime))
|
||||
blockTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Infof("[migration] commit %s changes", p.Sprintf("%d", count))
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Log().Error("[migration]", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Log().Infof("[migration] complete")
|
||||
}
|
||||
@@ -14,15 +14,13 @@ func searchParser(args []string, start, limit int) *sqlf.Stmt {
|
||||
}
|
||||
|
||||
q := sqlf.From("mailbox").
|
||||
Select(`ID, Data, Tags, Read,
|
||||
json_extract(Data, '$.To') as ToJSON,
|
||||
json_extract(Data, '$.From') as FromJSON,
|
||||
IFNULL(json_extract(Data, '$.Cc'), '{}') as CcJSON,
|
||||
IFNULL(json_extract(Data, '$.Bcc'), '{}') as BccJSON,
|
||||
json_extract(Data, '$.Subject') as Subject,
|
||||
json_extract(Data, '$.Attachments') as Attachments
|
||||
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags,
|
||||
IFNULL(json_extract(Metadata, '$.To'), '{}') as ToJSON,
|
||||
IFNULL(json_extract(Metadata, '$.From'), '{}') as FromJSON,
|
||||
IFNULL(json_extract(Metadata, '$.Cc'), '{}') as CcJSON,
|
||||
IFNULL(json_extract(Metadata, '$.Bcc'), '{}') as BccJSON
|
||||
`).
|
||||
OrderBy("Sort DESC").
|
||||
OrderBy("Created DESC").
|
||||
Limit(limit).
|
||||
Offset(start)
|
||||
|
||||
@@ -92,6 +90,15 @@ func searchParser(args []string, start, limit int) *sqlf.Stmt {
|
||||
q.Where("Subject LIKE ?", "%"+escPercentChar(w)+"%")
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(w, "message-id:") {
|
||||
w = cleanString(w[11:])
|
||||
if w != "" {
|
||||
if exclude {
|
||||
q.Where("MessageID NOT LIKE ?", "%"+escPercentChar(w)+"%")
|
||||
} else {
|
||||
q.Where("MessageID LIKE ?", "%"+escPercentChar(w)+"%")
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(w, "tag:") {
|
||||
w = cleanString(w[4:])
|
||||
if w != "" {
|
||||
@@ -122,9 +129,9 @@ func searchParser(args []string, start, limit int) *sqlf.Stmt {
|
||||
} else {
|
||||
// search text
|
||||
if exclude {
|
||||
q.Where("search NOT LIKE ?", "%"+cleanString(escPercentChar(w))+"%")
|
||||
q.Where("SearchText NOT LIKE ?", "%"+cleanString(escPercentChar(w))+"%")
|
||||
} else {
|
||||
q.Where("search LIKE ?", "%"+cleanString(escPercentChar(w))+"%")
|
||||
q.Where("SearchText LIKE ?", "%"+cleanString(escPercentChar(w))+"%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import (
|
||||
//
|
||||
// swagger:model Message
|
||||
type Message struct {
|
||||
// Unique message database id
|
||||
// Database ID
|
||||
ID string
|
||||
// Message ID
|
||||
MessageID string
|
||||
// Read status
|
||||
Read bool
|
||||
// From address
|
||||
@@ -25,7 +27,7 @@ type Message struct {
|
||||
Bcc []*mail.Address
|
||||
// ReplyTo addresses
|
||||
ReplyTo []*mail.Address
|
||||
// ReturnPath is the Return-Path
|
||||
// Return-Path
|
||||
ReturnPath string
|
||||
// Message subject
|
||||
Subject string
|
||||
@@ -49,15 +51,15 @@ type Message struct {
|
||||
//
|
||||
// swagger:model Attachment
|
||||
type Attachment struct {
|
||||
// attachment part id
|
||||
// Attachment part ID
|
||||
PartID string
|
||||
// file name
|
||||
// File name
|
||||
FileName string
|
||||
// content type
|
||||
// Content type
|
||||
ContentType string
|
||||
// content id
|
||||
// Content ID
|
||||
ContentID string
|
||||
// size in bytes
|
||||
// Size in bytes
|
||||
Size int
|
||||
}
|
||||
|
||||
@@ -65,8 +67,10 @@ type Attachment struct {
|
||||
//
|
||||
// swagger:model MessageSummary
|
||||
type MessageSummary struct {
|
||||
// Unique message database id
|
||||
// Database ID
|
||||
ID string
|
||||
// Message ID
|
||||
MessageID string
|
||||
// Read status
|
||||
Read bool
|
||||
// From address
|
||||
|
||||
@@ -3,27 +3,27 @@ package storage
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/axllent/mailpit/utils/tools"
|
||||
"github.com/leporo/sqlf"
|
||||
)
|
||||
|
||||
// SetTags will set the tags for a given message ID, used via API
|
||||
// SetTags will set the tags for a given database ID, used via API
|
||||
func SetTags(id string, tags []string) error {
|
||||
applyTags := []string{}
|
||||
reg := regexp.MustCompile(`\s+`)
|
||||
for _, t := range tags {
|
||||
t = strings.TrimSpace(reg.ReplaceAllString(t, " "))
|
||||
|
||||
if t != "" && config.TagRegexp.MatchString(t) && !inArray(t, applyTags) {
|
||||
t = tools.CleanTag(t)
|
||||
if t != "" && config.ValidTagRegexp.MatchString(t) && !inArray(t, applyTags) {
|
||||
applyTags = append(applyTags, t)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(applyTags)
|
||||
|
||||
tagJSON, err := json.Marshal(applyTags)
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] setting tags for message %s", id)
|
||||
@@ -42,26 +42,25 @@ func SetTags(id string, tags []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Used to auto-apply tags to new messages
|
||||
func findTags(message *[]byte) []string {
|
||||
tags := []string{}
|
||||
// Find tags set via --tags in raw message.
|
||||
// Returns a comma-separated string.
|
||||
func findTagsInRawMessage(message *[]byte) string {
|
||||
tagStr := ""
|
||||
if len(config.SMTPTags) == 0 {
|
||||
return tags
|
||||
return tagStr
|
||||
}
|
||||
|
||||
str := strings.ToLower(string(*message))
|
||||
for _, t := range config.SMTPTags {
|
||||
if !inArray(t.Tag, tags) && strings.Contains(str, t.Match) {
|
||||
tags = append(tags, t.Tag)
|
||||
if strings.Contains(str, t.Match) {
|
||||
tagStr += "," + t.Tag
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(tags)
|
||||
|
||||
return tags
|
||||
return tagStr
|
||||
}
|
||||
|
||||
// Get message tags from the database for a given message ID.
|
||||
// Get message tags from the database for a given database ID
|
||||
// Used when parsing a raw email.
|
||||
func getMessageTags(id string) []string {
|
||||
tags := []string{}
|
||||
@@ -84,3 +83,31 @@ func getMessageTags(id string) []string {
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
// UniqueTagsFromString will split a string with commas, and extract a unique slice of formatted tags
|
||||
func uniqueTagsFromString(s string) []string {
|
||||
tags := []string{}
|
||||
|
||||
if s == "" {
|
||||
return tags
|
||||
}
|
||||
|
||||
parts := strings.Split(s, ",")
|
||||
for _, p := range parts {
|
||||
w := tools.CleanTag(p)
|
||||
if w == "" {
|
||||
continue
|
||||
}
|
||||
if config.ValidTagRegexp.MatchString(w) {
|
||||
if !inArray(w, tags) {
|
||||
tags = append(tags, w)
|
||||
}
|
||||
} else {
|
||||
logger.Log().Debugf("[db] ignoring invalid tag: %s", w)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(tags)
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func createSearchText(env *enmime.Envelope) string {
|
||||
b.WriteString(env.GetHeader("Bcc") + " ")
|
||||
h := strings.TrimSpace(
|
||||
html2text.HTML2TextWithOptions(
|
||||
env.HTML,
|
||||
env.HTML,
|
||||
html2text.WithLinksInnerText(),
|
||||
),
|
||||
)
|
||||
@@ -75,7 +75,7 @@ func dbCron() {
|
||||
time.Sleep(60 * time.Second)
|
||||
start := time.Now()
|
||||
|
||||
// check if database contains deleted data and has not beein in use
|
||||
// check if database contains deleted data and has not been in use
|
||||
// for 5 minutes, if so VACUUM
|
||||
currentTime := time.Now()
|
||||
diff := currentTime.Sub(dbLastAction)
|
||||
@@ -92,7 +92,7 @@ func dbCron() {
|
||||
if config.MaxMessages > 0 {
|
||||
q := sqlf.Select("ID").
|
||||
From("mailbox").
|
||||
OrderBy("Sort DESC").
|
||||
OrderBy("Created DESC").
|
||||
Limit(5000).
|
||||
Offset(config.MaxMessages)
|
||||
|
||||
@@ -167,6 +167,7 @@ func isFile(path string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// InArray tests if a string in within an array. It is not case sensitive.
|
||||
func inArray(k string, arr []string) bool {
|
||||
k = strings.ToLower(k)
|
||||
for _, v := range arr {
|
||||
|
||||
@@ -54,7 +54,7 @@ func PrettyPrint(i interface{}) {
|
||||
}
|
||||
|
||||
// CleanIP returns a human-readable IP for the logging interface
|
||||
// when starting services. It translates [::]:<port> to "localhost:<port>"
|
||||
// when starting services. It translates [::]:<port> to "0.0.0.0:<port>"
|
||||
func CleanIP(s string) string {
|
||||
re := regexp.MustCompile(`^\[\:\:\]\:\d+`)
|
||||
if re.MatchString(s) {
|
||||
@@ -63,3 +63,14 @@ func CleanIP(s string) string {
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// CleanHTTPIP returns a human-readable IP for the logging interface
|
||||
// when starting services. It translates [::]:<port> to "localhost:<port>"
|
||||
func CleanHTTPIP(s string) string {
|
||||
re := regexp.MustCompile(`^\[\:\:\]\:\d+`)
|
||||
if re.MatchString(s) {
|
||||
return "localhost:" + s[5:]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package tools provides various methods for variouws things
|
||||
// Package tools provides various methods for various things
|
||||
package tools
|
||||
|
||||
import (
|
||||
@@ -23,7 +23,7 @@ func RemoveMessageHeaders(msg []byte, headers []string) ([]byte, error) {
|
||||
reBlank := regexp.MustCompile(`^\s+`)
|
||||
|
||||
for _, hdr := range headers {
|
||||
// case-insentitive
|
||||
// case-insensitive
|
||||
reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta(hdr+":"))
|
||||
|
||||
// header := []byte(hdr + ":")
|
||||
|
||||
25
utils/tools/tags.go
Normal file
25
utils/tools/tags.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// Invalid tag characters regex
|
||||
tagsInvalidChars = regexp.MustCompile(`[^a-zA-Z0-9\-\ \_]`)
|
||||
|
||||
// Regex to catch multiple spaces
|
||||
multiSpaceRe = regexp.MustCompile(`(\s+)`)
|
||||
)
|
||||
|
||||
// CleanTag returns a clean tag, removing whitespace and invalid characters
|
||||
func CleanTag(s string) string {
|
||||
s = strings.TrimSpace(
|
||||
multiSpaceRe.ReplaceAllString(
|
||||
tagsInvalidChars.ReplaceAllString(s, " "),
|
||||
" ",
|
||||
),
|
||||
)
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user