mirror of
https://github.com/axllent/mailpit.git
synced 2025-03-11 14:59:57 +02:00
First commit
This commit is contained in:
commit
7a9b11a9e5
42
.github/workflows/release-build.yml
vendored
Normal file
42
.github/workflows/release-build.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
name: Build & release
|
||||
jobs:
|
||||
releases-matrix:
|
||||
name: Release Go Binary
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin]
|
||||
goarch: ["386", amd64, arm64]
|
||||
exclude:
|
||||
- goarch: "386"
|
||||
goos: darwin
|
||||
- goarch: arm64
|
||||
goos: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
|
||||
# build the assets
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run package
|
||||
|
||||
# build the binaries
|
||||
- uses: wangyoucao577/go-release-action@v1.30
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
binary_name: "mailpit"
|
||||
extra_files: LICENSE README.md
|
||||
build_command: go build -trimpath -ldflags '-s -w -X github.com/axllent/mailpit/cmd.Version=${{ steps.tag.outputs.tag }}'
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/node_modules/
|
||||
/send
|
||||
/server/ui/dist
|
||||
/Makefile
|
||||
/mailpit
|
||||
*.old
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2022-Now() Ralph Slooten
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
45
README-BUILDING.md
Normal file
45
README-BUILDING.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Building Mailpit from source
|
||||
|
||||
Go (>= version 1.8) and npm are required to compile mailpit from source.
|
||||
|
||||
```
|
||||
git clone git@github.com:axllent/mailpit.git
|
||||
cd mailpit
|
||||
```
|
||||
|
||||
## Building the UI
|
||||
|
||||
The Mailpit web user interface is built with node. In the project's root (top) directory run the following to install the required node modules:
|
||||
|
||||
|
||||
### Installing the node modules
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
|
||||
### Building the web UI
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can also run `npm run watch` which will watch for changes and rebuild the HTML/CSS/JS automatically when changes are detected.
|
||||
Please note that you must restart Mailpit (`go run .`) to run with the changes.
|
||||
|
||||
|
||||
## Build the mailpit binary
|
||||
|
||||
One you have the assets compiled, you can build mailpit as follows:
|
||||
```
|
||||
go build -ldflags "-s -w"
|
||||
```
|
||||
|
||||
## Building a stand-alone sendmail binary
|
||||
|
||||
This step is unnecessary, however if you do not intend to either symlink `sendmail` to mailpit or configure your existing sendmail to route mail to mailpit, you can optionally build a stand-alone sendmail binary.
|
||||
|
||||
```
|
||||
cd sendmail
|
||||
go build -ldflags "-s -w"
|
||||
```
|
60
README.md
Normal file
60
README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Mailpit
|
||||
|
||||
Mailpit is an email testing tool for developers.
|
||||
|
||||
It acts as both an SMTP server, and provides a web interface to view all captured emails.
|
||||
|
||||
Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Runs completely on a single binary
|
||||
- SMTP server (default `127.0.0.1:1025`)
|
||||
- Web UI to view emails (HTML format, text, source and MIME attachments, default `127.0.0.1:8025`)
|
||||
- Real-time web UI updates using websockets for new mail
|
||||
- Email storage in either memory or disk (using [CloverDB](https://github.com/ostafen/clover)) - note that in-memory has a physical limit of 1MB per email
|
||||
- Configurable automatic email pruning (default keeps the most recent 500 emails)
|
||||
- Fast SMTP processing & storing - approximately 300-600 emails per second depending on CPU, network speed & email size
|
||||
- Can handle tens of thousands of emails
|
||||
|
||||
|
||||
## Planned features
|
||||
|
||||
- Optional HTTPS for web UI
|
||||
- Optional basic authentication for web UI
|
||||
- Optional authentication for SMTP
|
||||
- Browser notifications for new mail (HTTPS only)
|
||||
- Docker container
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Download a pre-built binary in the [releases](https://github.com/axllent/mailpit/releases/latest). The `mailpit` can be placed in your `$PATH`, or simply run as `./mailpit`. See `mailpit -h` for options.
|
||||
|
||||
To build mailpit from source see [building from source](README-BUILDING.md).
|
||||
|
||||
|
||||
### 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](README-BUILDING.md)).
|
||||
|
||||
|
||||
## Why rewrite MailHog?
|
||||
|
||||
I had been using MailHog for a few years to intercept and test emails generated from several projects. Mailhog has a number of severe performance issues, many of the modules are horribly out of date, and other than a few accepted MRs, it is not actively developed.
|
||||
|
||||
Initially I started trying to upgrade a fork of MailHog (both the UI as well as the HTTP server & API), but soon discovered that it is (with all due respect) very poorly designed. It is over-engineered (split over 9 separate projects), has too many unnecessary features for my purpose, and performs exceptionally poorly when dealing with large lumbers of emails or processing any email with an attachment (a single email with a 3MB attachment can take over a minute). The API transmits a lot of duplicate and unnecessary data on every message request for all web calls, and there is no HTTP compression.
|
||||
|
||||
In order to improve it I felt it needed to be completely rewritten, and so Mailpit was born.
|
86
cmd/root.go
Normal file
86
cmd/root.go
Normal file
@ -0,0 +1,86 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/mailpit/server"
|
||||
"github.com/axllent/mailpit/smtpd"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "mailpit",
|
||||
Short: "Mailpit is an email testing tool for developers",
|
||||
Long: `Mailpit is an email testing tool for developers.
|
||||
|
||||
It acts as an SMTP server, and provides a web interface to view all captured emails.`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
if err := config.VerifyConfig(); err != nil {
|
||||
logger.Log().Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := storage.InitDB(); err != nil {
|
||||
logger.Log().Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go server.Listen()
|
||||
|
||||
if err := smtpd.Listen(); err != nil {
|
||||
logger.Log().Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// SendmailExecute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func SendmailExecute() {
|
||||
args := []string{"mailpit", "sendmail"}
|
||||
|
||||
rootCmd.Run(sendmailCmd, args)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// hide autocompletion
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
// rootCmd.Flags().SortFlags = false
|
||||
// hide help
|
||||
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
|
||||
|
||||
// defaults from envars if provided
|
||||
if len(os.Getenv("MP_DATA_DIR")) > 0 {
|
||||
config.DataDir = os.Getenv("MP_DATA_DIR")
|
||||
}
|
||||
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
|
||||
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")
|
||||
}
|
||||
if len(os.Getenv("MP_UI_BIND_ADDR")) > 0 {
|
||||
config.HTTPListen = os.Getenv("MP_UI_BIND_ADDR")
|
||||
}
|
||||
if len(os.Getenv("MP_MAX_MESSAGES")) > 0 {
|
||||
config.MaxMessages, _ = strconv.Atoi(os.Getenv("MP_MAX_MESSAGES"))
|
||||
}
|
||||
|
||||
rootCmd.Flags().StringVarP(&config.DataDir, "data", "d", config.DataDir, "Optional path to store peristent data")
|
||||
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
|
||||
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
|
||||
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages per mailbox")
|
||||
rootCmd.Flags().BoolVarP(&config.VerboseLogging, "verbose", "v", false, "Verbose logging")
|
||||
}
|
33
cmd/sendmail.go
Normal file
33
cmd/sendmail.go
Normal file
@ -0,0 +1,33 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
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",
|
||||
Short: "A sendmail command replacement",
|
||||
Long: `A sendmail command replacement.
|
||||
|
||||
You can optionally create a symlink called 'sendmail' to the main binary.`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
sendmail.Run()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(sendmailCmd)
|
||||
|
||||
// these are simply repeated for cli consistency
|
||||
sendmailCmd.Flags().StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
|
||||
sendmailCmd.Flags().StringVarP(&fromAddr, "from", "f", "", "SMTP sender")
|
||||
sendmailCmd.Flags().BoolP("long-i", "i", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-t", "t", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
}
|
69
cmd/version.go
Normal file
69
cmd/version.go
Normal file
@ -0,0 +1,69 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/axllent/mailpit/updater"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version is the default application version, updated on release
|
||||
Version = "dev"
|
||||
|
||||
// Repo on Github for updater
|
||||
Repo = "axllent/mailpit"
|
||||
|
||||
// RepoBinaryName on Github for updater
|
||||
RepoBinaryName = "mailpit"
|
||||
)
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Display the current version & update information",
|
||||
Long: `Display the current version & update information (if available).`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
updater.AllowPrereleases = true
|
||||
|
||||
update, _ := cmd.Flags().GetBool("update")
|
||||
|
||||
if update {
|
||||
return updateApp()
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s compiled with %s on %s/%s\n",
|
||||
os.Args[0], Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
latest, _, _, err := updater.GithubLatest(Repo, RepoBinaryName)
|
||||
if err == nil && updater.GreaterThan(latest, Version) {
|
||||
fmt.Printf(
|
||||
"\nUpdate available: %s\nRun `%s version -u` to update (requires read/write access to install directory).\n",
|
||||
latest,
|
||||
os.Args[0],
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
versionCmd.Flags().
|
||||
BoolP("update", "u", false, "update to latest version")
|
||||
}
|
||||
|
||||
func updateApp() error {
|
||||
rel, err := updater.GithubUpdate(Repo, RepoBinaryName, Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Updated %s to version %s\n", os.Args[0], rel)
|
||||
return nil
|
||||
}
|
44
config/config.go
Normal file
44
config/config.go
Normal file
@ -0,0 +1,44 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// SMTPListen to listen on <interface>:<port>
|
||||
SMTPListen = "0.0.0.0:1025"
|
||||
|
||||
// HTTPListen to listen on <interface>:<port>
|
||||
HTTPListen = "0.0.0.0:8025"
|
||||
|
||||
// DataDir for mail (optional)
|
||||
DataDir string
|
||||
|
||||
// MaxMessages is the maximum number of messages a mailbox can have (auto-pruned every minute)
|
||||
MaxMessages = 500
|
||||
|
||||
// VerboseLogging for console output
|
||||
VerboseLogging = false
|
||||
|
||||
// NoLogging for testing
|
||||
NoLogging = false
|
||||
|
||||
// SSLCert @TODO
|
||||
SSLCert string
|
||||
// SSLKey @TODO
|
||||
SSLKey string
|
||||
)
|
||||
|
||||
// VerifyConfig wil do some basic checking
|
||||
func VerifyConfig() error {
|
||||
re := regexp.MustCompile(`^[a-zA-Z0-9\.\-]{3,}:\d{2,}$`)
|
||||
if !re.MatchString(SMTPListen) {
|
||||
return errors.New("SMTP bind should be in the format of <ip>:<port>")
|
||||
}
|
||||
if !re.MatchString(HTTPListen) {
|
||||
return errors.New("HTTP bind should be in the format of <ip>:<port>")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
18
data/mailbox.go
Normal file
18
data/mailbox.go
Normal file
@ -0,0 +1,18 @@
|
||||
package data
|
||||
|
||||
import "time"
|
||||
|
||||
// MailboxSummary struct
|
||||
type MailboxSummary struct {
|
||||
Name string
|
||||
Slug string
|
||||
Total int
|
||||
Unread int
|
||||
LastMessage time.Time
|
||||
}
|
||||
|
||||
// WebsocketNotification struct for responses
|
||||
type WebsocketNotification struct {
|
||||
Type string
|
||||
Data interface{}
|
||||
}
|
64
data/message.go
Normal file
64
data/message.go
Normal file
@ -0,0 +1,64 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
// Message struct for loading messages. It does not include physical attachments.
|
||||
type Message struct {
|
||||
ID string
|
||||
Read bool
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
Subject string
|
||||
Date time.Time
|
||||
Created time.Time
|
||||
Text string
|
||||
HTML string
|
||||
Size int
|
||||
Inline []Attachment
|
||||
Attachments []Attachment
|
||||
}
|
||||
|
||||
// Attachment struct for inline and attachments
|
||||
type Attachment struct {
|
||||
PartID string
|
||||
FileName string
|
||||
ContentType string
|
||||
ContentID string
|
||||
Size int
|
||||
}
|
||||
|
||||
// Summary struct for frontend messages
|
||||
type Summary struct {
|
||||
ID string
|
||||
Read bool
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
Subject string
|
||||
Created time.Time
|
||||
Size int
|
||||
Attachments int
|
||||
}
|
||||
|
||||
// AttachmentSummary returns a summary of the attachment without any binary data
|
||||
func AttachmentSummary(a *enmime.Part) Attachment {
|
||||
o := Attachment{}
|
||||
o.PartID = a.PartID
|
||||
o.FileName = a.FileName
|
||||
if o.FileName == "" {
|
||||
o.FileName = a.ContentID
|
||||
}
|
||||
o.ContentType = a.ContentType
|
||||
o.ContentID = a.ContentID
|
||||
o.Size = len(a.Content)
|
||||
|
||||
return o
|
||||
}
|
22
esbuild.config.js
Normal file
22
esbuild.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { build } = require('esbuild')
|
||||
const pluginVue = require('esbuild-plugin-vue-next')
|
||||
const sassPlugin = require("esbuild-plugin-sass");
|
||||
|
||||
const doWatch = process.env.WATCH == 'true' ? true : false;
|
||||
const doMinify = process.env.MINIFY == 'true' ? true : false;
|
||||
|
||||
build({
|
||||
entryPoints: ["server/ui-src/app.js"],
|
||||
bundle: true,
|
||||
watch: doWatch,
|
||||
minify: doMinify,
|
||||
sourcemap: false,
|
||||
outfile: "server/ui/dist/app.js",
|
||||
plugins: [pluginVue(), sassPlugin()],
|
||||
loader: {
|
||||
".svg": "file",
|
||||
".woff": "file",
|
||||
".woff2": "file",
|
||||
},
|
||||
logLevel: "info"
|
||||
})
|
52
go.mod
Normal file
52
go.mod
Normal file
@ -0,0 +1,52 @@
|
||||
module github.com/axllent/mailpit
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/axllent/semver v0.0.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jhillyerd/enmime v0.10.0
|
||||
github.com/k3a/html2text v1.0.8
|
||||
github.com/mhale/smtpd v0.8.0
|
||||
github.com/ostafen/clover v1.2.1-0.20220728200552-0b95f72b304c
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v2.0.6+incompatible // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/google/orderedcode v0.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba // indirect
|
||||
github.com/klauspost/compress v1.15.9 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.3.0 // indirect
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/testify v1.7.2 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220726230323-06994584191e // indirect
|
||||
golang.org/x/sys v0.0.0-20220727055044-e65921a090b8 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
)
|
291
go.sum
Normal file
291
go.sum
Normal file
@ -0,0 +1,291 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E=
|
||||
github.com/axllent/semver v0.0.1/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc=
|
||||
github.com/brianvoe/gofakeit/v6 v6.17.0 h1:obbQTJeHfktJtiZzq0Q1bEpsNUs+yHrYlPVWt7BtmJ4=
|
||||
github.com/brianvoe/gofakeit/v6 v6.17.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
|
||||
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v2.0.6+incompatible h1:XHFReMv7nFFusa+CEokzWbzaYocKXI6C7hdU5Kgh9Lw=
|
||||
github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us=
|
||||
github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jhillyerd/enmime v0.10.0 h1:DZEzhptPRBesvN3gf7K1BOh4rfpqdsdrEoxW1Edr/3s=
|
||||
github.com/jhillyerd/enmime v0.10.0/go.mod h1:Qpe8EEemJMFAF8+NZoWdpXvK2Yb9dRF0k/z6mkcDHsA=
|
||||
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.0.8 h1:rVanLhKilpnJUJs/CNKWzMC4YaQINGxK0rSG8ssmnV0=
|
||||
github.com/k3a/html2text v1.0.8/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mhale/smtpd v0.8.0 h1:5JvdsehCg33PQrZBvFyDMMUDQmvbzVpZgKob7eYBJc0=
|
||||
github.com/mhale/smtpd v0.8.0/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/ostafen/clover v1.2.1-0.20220728200552-0b95f72b304c h1:hFGWRJPoIP3e73jFTdeMTyG1kwoe7r5Ayf1o9Wqyqh8=
|
||||
github.com/ostafen/clover v1.2.1-0.20220728200552-0b95f72b304c/go.mod h1:KVMcjgoq15v0S/I0GGAZPPtwO6+w6rYM0ZW/6XSO2Ic=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.3.0 h1:eyC18g7xB83Dv/xlJXLgNkRidVoR7nqFZBJvqo/K188=
|
||||
github.com/rivo/uniseg v0.3.0/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
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/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=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220726230323-06994584191e h1:wOQNKh1uuDGRnmgF0jDxh7ctgGy/3P4rYWQRVJD4/Yg=
|
||||
golang.org/x/net v0.0.0-20220726230323-06994584191e/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220727055044-e65921a090b8 h1:dyU22nBWzrmTQxtNrr4dzVOvaw35nUYE279vF9UmsI8=
|
||||
golang.org/x/sys v0.0.0-20220727055044-e65921a090b8/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/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
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=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
43
logger/logger.go
Normal file
43
logger/logger.go
Normal file
@ -0,0 +1,43 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
log *logrus.Logger
|
||||
)
|
||||
|
||||
// Log returns the logger instance
|
||||
func Log() *logrus.Logger {
|
||||
if log == nil {
|
||||
log = logrus.New()
|
||||
log.SetLevel(logrus.InfoLevel)
|
||||
if config.VerboseLogging {
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
if config.NoLogging {
|
||||
log.SetLevel(logrus.PanicLevel)
|
||||
}
|
||||
|
||||
log.Out = os.Stdout
|
||||
log.SetFormatter(&logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "15:04:05",
|
||||
ForceColors: true,
|
||||
})
|
||||
}
|
||||
|
||||
return log
|
||||
}
|
||||
|
||||
// PrettyPrint for debugging
|
||||
func PrettyPrint(i interface{}) {
|
||||
s, _ := json.MarshalIndent(i, "", "\t")
|
||||
fmt.Println(string(s))
|
||||
}
|
24
main.go
Normal file
24
main.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/axllent/mailpit/cmd"
|
||||
sendmail "github.com/axllent/mailpit/sendmail/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
exec, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// running directly
|
||||
if filepath.Base(exec) == filepath.Base(os.Args[0]) {
|
||||
cmd.Execute()
|
||||
} else {
|
||||
// symlinked
|
||||
sendmail.Run()
|
||||
}
|
||||
}
|
806
package-lock.json
generated
Normal file
806
package-lock.json
generated
Normal file
@ -0,0 +1,806 @@
|
||||
{
|
||||
"name": "mailpit",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": {
|
||||
"version": "7.18.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.8.tgz",
|
||||
"integrity": "sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA=="
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
|
||||
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
|
||||
"dev": true
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.37.tgz",
|
||||
"integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/shared": "3.2.37",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz",
|
||||
"integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==",
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz",
|
||||
"integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.37",
|
||||
"@vue/compiler-dom": "3.2.37",
|
||||
"@vue/compiler-ssr": "3.2.37",
|
||||
"@vue/reactivity-transform": "3.2.37",
|
||||
"@vue/shared": "3.2.37",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7",
|
||||
"postcss": "^8.1.10",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz",
|
||||
"integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
}
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.37.tgz",
|
||||
"integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==",
|
||||
"requires": {
|
||||
"@vue/shared": "3.2.37"
|
||||
}
|
||||
},
|
||||
"@vue/reactivity-transform": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz",
|
||||
"integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.37",
|
||||
"@vue/shared": "3.2.37",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-core": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.37.tgz",
|
||||
"integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==",
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-dom": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz",
|
||||
"integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==",
|
||||
"requires": {
|
||||
"@vue/runtime-core": "3.2.37",
|
||||
"@vue/shared": "3.2.37",
|
||||
"csstype": "^2.6.8"
|
||||
}
|
||||
},
|
||||
"@vue/server-renderer": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.37.tgz",
|
||||
"integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==",
|
||||
"requires": {
|
||||
"@vue/compiler-ssr": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
|
||||
"integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw=="
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.0.tgz",
|
||||
"integrity": "sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A=="
|
||||
},
|
||||
"bootstrap-icons": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.9.1.tgz",
|
||||
"integrity": "sha512-d4ZkO30MIkAhQ2nNRJqKXJVEQorALGbLWTuRxyCTJF96lRIV6imcgMehWGJUiJMJhglN0o2tqLIeDnMdiQEE9g=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"chainsaw": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.0.9.tgz",
|
||||
"integrity": "sha512-nG8PYH+/4xB+8zkV4G844EtfvZ5tTiLFoX3dZ4nhF4t3OCKIb9UvaFyNmeZO2zOSmRWzBoTD+napN6hiL+EgcA==",
|
||||
"requires": {
|
||||
"traverse": ">=0.3.0 <0.4"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"convert-source-map": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
|
||||
"integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.1"
|
||||
}
|
||||
},
|
||||
"css-tree": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mdn-data": "2.0.14",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "2.6.20",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
|
||||
"integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.50.tgz",
|
||||
"integrity": "sha512-SbC3k35Ih2IC6trhbMYW7hYeGdjPKf9atTKwBUHqMCYFZZ9z8zhuvfnZihsnJypl74FjiAKjBRqFkBkAd0rS/w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild-android-64": "0.14.50",
|
||||
"esbuild-android-arm64": "0.14.50",
|
||||
"esbuild-darwin-64": "0.14.50",
|
||||
"esbuild-darwin-arm64": "0.14.50",
|
||||
"esbuild-freebsd-64": "0.14.50",
|
||||
"esbuild-freebsd-arm64": "0.14.50",
|
||||
"esbuild-linux-32": "0.14.50",
|
||||
"esbuild-linux-64": "0.14.50",
|
||||
"esbuild-linux-arm": "0.14.50",
|
||||
"esbuild-linux-arm64": "0.14.50",
|
||||
"esbuild-linux-mips64le": "0.14.50",
|
||||
"esbuild-linux-ppc64le": "0.14.50",
|
||||
"esbuild-linux-riscv64": "0.14.50",
|
||||
"esbuild-linux-s390x": "0.14.50",
|
||||
"esbuild-netbsd-64": "0.14.50",
|
||||
"esbuild-openbsd-64": "0.14.50",
|
||||
"esbuild-sunos-64": "0.14.50",
|
||||
"esbuild-windows-32": "0.14.50",
|
||||
"esbuild-windows-64": "0.14.50",
|
||||
"esbuild-windows-arm64": "0.14.50"
|
||||
}
|
||||
},
|
||||
"esbuild-android-64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.50.tgz",
|
||||
"integrity": "sha512-H7iUEm7gUJHzidsBlFPGF6FTExazcgXL/46xxLo6i6bMtPim6ZmXyTccS8yOMpy6HAC6dPZ/JCQqrkkin69n6Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-android-arm64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.50.tgz",
|
||||
"integrity": "sha512-NFaoqEwa+OYfoYVpQWDMdKII7wZZkAjtJFo1WdnBeCYlYikvUhTnf2aPwPu5qEAw/ie1NYK0yn3cafwP+kP+OQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.50.tgz",
|
||||
"integrity": "sha512-gDQsCvGnZiJv9cfdO48QqxkRV8oKAXgR2CGp7TdIpccwFdJMHf8hyIJhMW/05b/HJjET/26Us27Jx91BFfEVSA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-arm64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.50.tgz",
|
||||
"integrity": "sha512-36nNs5OjKIb/Q50Sgp8+rYW/PqirRiFN0NFc9hEvgPzNJxeJedktXwzfJSln4EcRFRh5Vz4IlqFRScp+aiBBzA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.50.tgz",
|
||||
"integrity": "sha512-/1pHHCUem8e/R86/uR+4v5diI2CtBdiWKiqGuPa9b/0x3Nwdh5AOH7lj+8823C6uX1e0ufwkSLkS+aFZiBCWxA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-arm64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.50.tgz",
|
||||
"integrity": "sha512-iKwUVMQztnPZe5pUYHdMkRc9aSpvoV1mkuHlCoPtxZA3V+Kg/ptpzkcSY+fKd0kuom+l6Rc93k0UPVkP7xoqrw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-32": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.50.tgz",
|
||||
"integrity": "sha512-sWUwvf3uz7dFOpLzYuih+WQ7dRycrBWHCdoXJ4I4XdMxEHCECd8b7a9N9u7FzT6XR2gHPk9EzvchQUtiEMRwqw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.50.tgz",
|
||||
"integrity": "sha512-u0PQxPhaeI629t4Y3EEcQ0wmWG+tC/LpP2K7yDFvwuPq0jSQ8SIN+ARNYfRjGW15O2we3XJvklbGV0wRuUCPig==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.50.tgz",
|
||||
"integrity": "sha512-VALZq13bhmFJYFE/mLEb+9A0w5vo8z+YDVOWeaf9vOTrSC31RohRIwtxXBnVJ7YKLYfEMzcgFYf+OFln3Y0cWg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.50.tgz",
|
||||
"integrity": "sha512-ZyfoNgsTftD7Rp5S7La5auomKdNeB3Ck+kSKXC4pp96VnHyYGjHHXWIlcbH8i+efRn9brszo1/Thl1qn8RqmhQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-mips64le": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.50.tgz",
|
||||
"integrity": "sha512-ygo31Vxn/WrmjKCHkBoutOlFG5yM9J2UhzHb0oWD9O61dGg+Hzjz9hjf5cmM7FBhAzdpOdEWHIrVOg2YAi6rTw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-ppc64le": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.50.tgz",
|
||||
"integrity": "sha512-xWCKU5UaiTUT6Wz/O7GKP9KWdfbsb7vhfgQzRfX4ahh5NZV4ozZ4+SdzYG8WxetsLy84UzLX3Pi++xpVn1OkFQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-riscv64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.50.tgz",
|
||||
"integrity": "sha512-0+dsneSEihZTopoO9B6Z6K4j3uI7EdxBP7YSF5rTwUgCID+wHD3vM1gGT0m+pjCW+NOacU9kH/WE9N686FHAJg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-s390x": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.50.tgz",
|
||||
"integrity": "sha512-tVjqcu8o0P9H4StwbIhL1sQYm5mWATlodKB6dpEZFkcyTI8kfIGWiWcrGmkNGH2i1kBUOsdlBafPxR3nzp3TDA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-netbsd-64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.50.tgz",
|
||||
"integrity": "sha512-0R/glfqAQ2q6MHDf7YJw/TulibugjizBxyPvZIcorH0Mb7vSimdHy0XF5uCba5CKt+r4wjax1mvO9lZ4jiAhEg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-openbsd-64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.50.tgz",
|
||||
"integrity": "sha512-7PAtmrR5mDOFubXIkuxYQ4bdNS6XCK8AIIHUiZxq1kL8cFIH5731jPcXQ4JNy/wbj1C9sZ8rzD8BIM80Tqk29w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-plugin-sass": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-plugin-sass/-/esbuild-plugin-sass-1.0.1.tgz",
|
||||
"integrity": "sha512-YFxjzD9Z1vz92QCJcAmCO15WVCUiOobw9ypdVeMsW+xa6S+zqryLUIh8d3fe/UkRHRO5PODZz/3xDAQuEXZwmQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css-tree": "1.1.3",
|
||||
"fs-extra": "10.0.0",
|
||||
"sass": "1.47.0",
|
||||
"tmp": "0.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"sass": {
|
||||
"version": "1.47.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.47.0.tgz",
|
||||
"integrity": "sha512-GtXwvwgD7/6MLUZPnlA5/8cdRgC9SzT5kAnnJMRmEZQFRE3J56Foswig4NyyyQGsnmNvg6EUM/FP0Pe9Y2zywQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"esbuild-plugin-vue-next": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-plugin-vue-next/-/esbuild-plugin-vue-next-0.1.4.tgz",
|
||||
"integrity": "sha512-n4DF5xY/GJ9DdRM4+MvV14Rrr+7xGhtv9/0xIxfzN6qSIMdXfZ6g4PVX735NYC7vGRr9KyZGRWST5jCyHQ6n5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"convert-source-map": "^1.8.0",
|
||||
"hash-sum": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"hash-sum": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
|
||||
"integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"esbuild-sunos-64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.50.tgz",
|
||||
"integrity": "sha512-gBxNY/wyptvD7PkHIYcq7se6SQEXcSC8Y7mE0FJB+CGgssEWf6vBPfTTZ2b6BWKnmaP6P6qb7s/KRIV5T2PxsQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-32": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.50.tgz",
|
||||
"integrity": "sha512-MOOe6J9cqe/iW1qbIVYSAqzJFh0p2LBLhVUIWdMVnNUNjvg2/4QNX4oT4IzgDeldU+Bym9/Tn6+DxvUHJXL5Zw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.50.tgz",
|
||||
"integrity": "sha512-r/qE5Ex3w1jjGv/JlpPoWB365ldkppUlnizhMxJgojp907ZF1PgLTuW207kgzZcSCXyquL9qJkMsY+MRtaZ5yQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-arm64": {
|
||||
"version": "0.14.50",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.50.tgz",
|
||||
"integrity": "sha512-EMS4lQnsIe12ZyAinOINx7eq2mjpDdhGZZWDwPZE/yUTN9cnc2Ze/xUTYIAyaJqrqQda3LnDpADKpvLvol6ENQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
|
||||
"integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.10",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
|
||||
"dev": true
|
||||
},
|
||||
"hashish": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz",
|
||||
"integrity": "sha512-xyD4XgslstNAs72ENaoFvgMwtv8xhiDtC2AtzCG+8yF7W/Knxxm9BX+e2s25mm+HxMKh0rBmXVOEGF3zNImXvA==",
|
||||
"requires": {
|
||||
"traverse": ">=0.2.4"
|
||||
}
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
|
||||
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
|
||||
"dev": true
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6",
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||
"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
|
||||
"requires": {
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
}
|
||||
},
|
||||
"mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
|
||||
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/remove/-/remove-0.1.5.tgz",
|
||||
"integrity": "sha512-AJMA9oWvJzdTjwIGwSQZsjGQiRx73YTmiOWmfCp1fpLa/D4n7jKcpoA+CZiVLJqKcEKUuh1Suq80c5wF+L/qVQ==",
|
||||
"requires": {
|
||||
"seq": ">= 0.3.5"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
},
|
||||
"seq": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/seq/-/seq-0.3.5.tgz",
|
||||
"integrity": "sha512-sisY2Ln1fj43KBkRtXkesnRHYNdswIkIibvNe/0UKm2GZxjMbqmccpiatoKr/k2qX5VKiLU8xm+tz/74LAho4g==",
|
||||
"requires": {
|
||||
"chainsaw": ">=0.0.7 <0.1",
|
||||
"hashish": ">=0.0.2 <0.1"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
|
||||
},
|
||||
"sourcemap-codec": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"rimraf": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"traverse": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
||||
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="
|
||||
},
|
||||
"universalify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
|
||||
"dev": true
|
||||
},
|
||||
"vue": {
|
||||
"version": "3.2.37",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz",
|
||||
"integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.2.37",
|
||||
"@vue/compiler-sfc": "3.2.37",
|
||||
"@vue/runtime-dom": "3.2.37",
|
||||
"@vue/server-renderer": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "mailpit",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node esbuild.config.js",
|
||||
"watch": "WATCH=true node esbuild.config.js",
|
||||
"package": "MINIFY=true node esbuild.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.2.0",
|
||||
"bootstrap-icons": "^1.9.1",
|
||||
"moment": "^2.29.4",
|
||||
"remove": "^0.1.5",
|
||||
"vue": "^3.2.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@vue/compiler-sfc": "^3.2.37",
|
||||
"esbuild": "^0.14.50",
|
||||
"esbuild-plugin-sass": "^1.0.1",
|
||||
"esbuild-plugin-vue-next": "^0.1.4"
|
||||
}
|
||||
}
|
85
sendmail/cmd/cmd.go
Normal file
85
sendmail/cmd/cmd.go
Normal file
@ -0,0 +1,85 @@
|
||||
package cmd
|
||||
|
||||
/**
|
||||
* Bare bones sendmail drop-in replacement borrowed from Mailhog
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Run the Mailpit sendmail replacement.
|
||||
func Run() {
|
||||
host, err := os.Hostname()
|
||||
if err != nil {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
username := "nobody"
|
||||
user, err := user.Current()
|
||||
if err == nil && user != nil && len(user.Username) > 0 {
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
fromAddr := username + "@" + host
|
||||
smtpAddr := "localhost:1025"
|
||||
var recip []string
|
||||
|
||||
// defaults from envars if provided
|
||||
if len(os.Getenv("MP_SENDMAIL_SMTP_ADDR")) > 0 {
|
||||
smtpAddr = os.Getenv("MP_SENDMAIL_SMTP_ADDR")
|
||||
}
|
||||
if len(os.Getenv("MP_SENDMAIL_FROM")) > 0 {
|
||||
fromAddr = os.Getenv("MP_SENDMAIL_FROM")
|
||||
}
|
||||
|
||||
var verbose bool
|
||||
|
||||
// override defaults from cli flags
|
||||
flag.StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
|
||||
flag.StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender")
|
||||
flag.BoolP("long-i", "i", true, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-t", "t", true, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolVarP(&verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
|
||||
flag.Parse()
|
||||
|
||||
// allow recipient to be passed as an argument
|
||||
recip = flag.Args()
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintln(os.Stderr, smtpAddr, fromAddr)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error reading stdin")
|
||||
os.Exit(11)
|
||||
}
|
||||
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("error parsing message body: %s", err))
|
||||
os.Exit(11)
|
||||
}
|
||||
|
||||
if len(recip) == 0 {
|
||||
// We only need to parse the message to get a recipient if none where
|
||||
// provided on the command line.
|
||||
recip = append(recip, msg.Header.Get("To"))
|
||||
}
|
||||
|
||||
err = smtp.SendMail(smtpAddr, nil, fromAddr, recip, body)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error sending mail")
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
7
sendmail/main.go
Normal file
7
sendmail/main.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/axllent/mailpit/sendmail/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Run()
|
||||
}
|
235
server/api.go
Normal file
235
server/api.go
Normal file
@ -0,0 +1,235 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/data"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type messagesResult struct {
|
||||
Total int `json:"total"`
|
||||
Count int `json:"count"`
|
||||
Start int `json:"start"`
|
||||
Items []data.Summary `json:"items"`
|
||||
}
|
||||
|
||||
// Return a list of available mailboxes
|
||||
func apiListMailboxes(w http.ResponseWriter, _ *http.Request) {
|
||||
res, err := storage.ListMailboxes()
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(res)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(bytes)
|
||||
}
|
||||
|
||||
func apiListMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
mailbox := vars["mailbox"]
|
||||
|
||||
if !storage.MailboxExists(mailbox) {
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
start, limit := getStartLimit(r)
|
||||
|
||||
messages, err := storage.List(mailbox, start, limit)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
total, err := storage.Count(mailbox)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var res messagesResult
|
||||
|
||||
res.Start = start
|
||||
res.Items = messages
|
||||
res.Count = len(res.Items)
|
||||
res.Total = total
|
||||
|
||||
bytes, _ := json.Marshal(res)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(bytes)
|
||||
}
|
||||
|
||||
func apiSearchMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||
if search == "" {
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
mailbox := vars["mailbox"]
|
||||
|
||||
if !storage.MailboxExists(mailbox) {
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
// we will only return up to 200 results
|
||||
start := 0
|
||||
limit := 200
|
||||
|
||||
messages, err := storage.Search(mailbox, search, start, limit)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
total, err := storage.Count(mailbox)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// total := limit
|
||||
// count := len(messages)
|
||||
// if total > count {
|
||||
// total = count
|
||||
// }
|
||||
|
||||
var res messagesResult
|
||||
|
||||
res.Start = start
|
||||
res.Items = messages
|
||||
res.Count = len(messages)
|
||||
res.Total = total
|
||||
|
||||
bytes, _ := json.Marshal(res)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(bytes)
|
||||
}
|
||||
|
||||
// Open a message
|
||||
func apiOpenMessage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
mailbox := vars["mailbox"]
|
||||
id := vars["id"]
|
||||
|
||||
msg, err := storage.GetMessage(mailbox, id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(msg)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(bytes)
|
||||
}
|
||||
|
||||
// Download/view an attachment
|
||||
func apiDownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
mailbox := vars["mailbox"]
|
||||
id := vars["id"]
|
||||
partID := vars["partID"]
|
||||
|
||||
a, err := storage.GetAttachmentPart(mailbox, id, partID)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
fileName := a.FileName
|
||||
if fileName == "" {
|
||||
fileName = a.ContentID
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", a.ContentType)
|
||||
w.Header().Set("Content-Disposition", "filename=\""+fileName+"\"")
|
||||
w.Write(a.Content)
|
||||
}
|
||||
|
||||
// View the full email source as plain text
|
||||
func apiDownloadSource(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
mailbox := vars["mailbox"]
|
||||
id := vars["id"]
|
||||
|
||||
dl := r.FormValue("dl")
|
||||
|
||||
data, err := storage.GetMessageRaw(mailbox, id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if dl == "1" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
|
||||
}
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// Delete all messages in the mailbox
|
||||
func apiDeleteAll(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
mailbox := vars["mailbox"]
|
||||
|
||||
err := storage.DeleteAllMessages(mailbox)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Delete a single message
|
||||
func apiDeleteOne(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
mailbox := vars["mailbox"]
|
||||
id := vars["id"]
|
||||
|
||||
err := storage.DeleteOneMessage(mailbox, id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Mark single message as unread
|
||||
func apiUnreadOne(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
mailbox := vars["mailbox"]
|
||||
id := vars["id"]
|
||||
|
||||
err := storage.UnreadMessage(mailbox, id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Websocket to broadcast changes
|
||||
func apiWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
websockets.ServeWs(websockets.MessageHub, w, r)
|
||||
}
|
131
server/server.go
Normal file
131
server/server.go
Normal file
@ -0,0 +1,131 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
//go:embed ui
|
||||
var embeddedFS embed.FS
|
||||
|
||||
// Listen will start the httpd
|
||||
func Listen() {
|
||||
serverRoot, err := fs.Sub(embeddedFS, "ui")
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[http] %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
websockets.MessageHub = websockets.NewHub()
|
||||
|
||||
go websockets.MessageHub.Run()
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/api/mailboxes", gzipHandlerFunc(apiListMailboxes))
|
||||
r.HandleFunc("/api/{mailbox}/messages", gzipHandlerFunc(apiListMailbox))
|
||||
r.HandleFunc("/api/{mailbox}/search", gzipHandlerFunc(apiSearchMailbox))
|
||||
r.HandleFunc("/api/{mailbox}/delete", gzipHandlerFunc(apiDeleteAll))
|
||||
r.HandleFunc("/api/{mailbox}/events", apiWebsocket)
|
||||
r.HandleFunc("/api/{mailbox}/{id}/source", gzipHandlerFunc(apiDownloadSource))
|
||||
r.HandleFunc("/api/{mailbox}/{id}/part/{partID}", gzipHandlerFunc(apiDownloadAttachment))
|
||||
r.HandleFunc("/api/{mailbox}/{id}/delete", gzipHandlerFunc(apiDeleteOne))
|
||||
r.HandleFunc("/api/{mailbox}/{id}/unread", gzipHandlerFunc(apiUnreadOne))
|
||||
r.HandleFunc("/api/{mailbox}/{id}", gzipHandlerFunc(apiOpenMessage))
|
||||
r.HandleFunc("/api/{mailbox}/search", gzipHandlerFunc(apiSearchMailbox))
|
||||
r.PathPrefix("/").Handler(gzipHandler(http.FileServer(http.FS(serverRoot))))
|
||||
http.Handle("/", r)
|
||||
|
||||
if config.SSLCert != "" && config.SSLKey != "" {
|
||||
logger.Log().Infof("[http] starting secure server on https://%s", config.HTTPListen)
|
||||
log.Fatal(http.ListenAndServeTLS(config.HTTPListen, config.SSLCert, config.SSLKey, nil))
|
||||
} else {
|
||||
logger.Log().Infof("[http] starting server on http://%s", config.HTTPListen)
|
||||
log.Fatal(http.ListenAndServe(config.HTTPListen, nil))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type gzipResponseWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
// GzipHandlerFunc http middleware
|
||||
func gzipHandlerFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
fn(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzip.NewWriter(w)
|
||||
defer gz.Close()
|
||||
gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||
fn(gzr, r)
|
||||
}
|
||||
}
|
||||
|
||||
func gzipHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzip.NewWriter(w)
|
||||
defer gz.Close()
|
||||
h.ServeHTTP(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r)
|
||||
})
|
||||
}
|
||||
|
||||
// FourOFour returns a standard 404 meesage
|
||||
func fourOFour(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprint(w, "404 page not found")
|
||||
}
|
||||
|
||||
// HTTPError returns a standard 404 meesage
|
||||
func httpError(w http.ResponseWriter, msg string) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprint(w, msg)
|
||||
}
|
||||
|
||||
// Get the start and limit based on query params. Defaults to 0, 50
|
||||
func getStartLimit(req *http.Request) (start int, limit int) {
|
||||
start = 0
|
||||
limit = 50
|
||||
|
||||
s := req.URL.Query().Get("start")
|
||||
if n, e := strconv.ParseInt(s, 10, 64); e == nil && n > 0 {
|
||||
start = int(n)
|
||||
}
|
||||
|
||||
l := req.URL.Query().Get("limit")
|
||||
if n, e := strconv.ParseInt(l, 10, 64); e == nil && n > 0 {
|
||||
if n > 500 {
|
||||
n = 500
|
||||
}
|
||||
limit = int(n)
|
||||
}
|
||||
|
||||
return start, limit
|
||||
}
|
416
server/ui-src/App.vue
Normal file
416
server/ui-src/App.vue
Normal file
@ -0,0 +1,416 @@
|
||||
<script>
|
||||
import commonMixins from './mixins.js'
|
||||
import Message from './templates/Message.vue';
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
mixins: [commonMixins],
|
||||
components: {
|
||||
Message
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentPath: window.location.hash,
|
||||
mailbox: "catchall",
|
||||
items: [],
|
||||
limit: 50,
|
||||
total: 0,
|
||||
start: 0,
|
||||
search: "",
|
||||
searching: false,
|
||||
isConnected: false,
|
||||
scrollInPlace: false,
|
||||
message: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentPath(v, old) {
|
||||
if (v && v.match(/^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/)) {
|
||||
this.openMessage();
|
||||
} else {
|
||||
this.message = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canPrev: function () {
|
||||
return this.start > 0;
|
||||
},
|
||||
canNext: function () {
|
||||
return this.total > (this.start + this.count);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.currentPath = window.location.hash.slice(1);
|
||||
window.addEventListener('hashchange', () => {
|
||||
this.currentPath = window.location.hash.slice(1);
|
||||
});
|
||||
|
||||
this.connect();
|
||||
this.loadMessages();
|
||||
},
|
||||
methods: {
|
||||
loadMessages: function () {
|
||||
let self = this;
|
||||
let params = {};
|
||||
|
||||
let uri = 'api/'+self.mailbox+'/messages';
|
||||
if (self.search) {
|
||||
self.searching = true;
|
||||
self.items = [];
|
||||
uri = 'api/'+self.mailbox+'/search'
|
||||
self.start = 0; // search is displayed on one page
|
||||
params['query'] = self.search;
|
||||
} else {
|
||||
self.searching = false;
|
||||
params['limit'] = self.limit;
|
||||
if (self.start > 0) {
|
||||
params['start'] = self.start;
|
||||
}
|
||||
}
|
||||
|
||||
self.get(uri, params, function(response){
|
||||
self.total = response.data.total;
|
||||
self.count = response.data.count;
|
||||
self.start = response.data.start;
|
||||
self.items = response.data.items;
|
||||
|
||||
if (!self.scrollInPlace) {
|
||||
let mp = document.getElementById('message-page');
|
||||
if (mp) {
|
||||
mp.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
self.scrollInPlace = false
|
||||
});
|
||||
},
|
||||
|
||||
doSearch: function(e) {
|
||||
e.preventDefault();
|
||||
this.loadMessages();
|
||||
},
|
||||
|
||||
reloadMessages: function() {
|
||||
this.search = "";
|
||||
this.start = 0;
|
||||
this.loadMessages();
|
||||
},
|
||||
|
||||
viewNext: function () {
|
||||
this.start = parseInt(this.start, 10) + parseInt(this.limit, 10);
|
||||
this.loadMessages();
|
||||
},
|
||||
|
||||
viewPrev: function () {
|
||||
let s = this.start - this.limit;
|
||||
if (s < 0) {
|
||||
s = 0;
|
||||
}
|
||||
this.start = s;
|
||||
this.loadMessages();
|
||||
},
|
||||
|
||||
openMessage: function(id) {
|
||||
let self = this;
|
||||
let params = {};
|
||||
|
||||
let uri = 'api/' + self.mailbox + '/' + self.currentPath
|
||||
self.get(uri, params, function(response) {
|
||||
for (let i in self.items) {
|
||||
if (self.items[i].ID == self.currentPath) {
|
||||
self.items[i].Read = true;
|
||||
}
|
||||
}
|
||||
let d = response.data;
|
||||
// replace inline images
|
||||
if (d.HTML && d.Inline) {
|
||||
for (let i in d.Inline) {
|
||||
let a = d.Inline[i];
|
||||
if (a.ContentID != '') {
|
||||
d.HTML = d.HTML.replace(
|
||||
new RegExp('cid:'+a.ContentID, 'g'),
|
||||
window.location.origin+'/api/'+self.mailbox+'/'+d.ID+'/part/'+a.PartID
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// replace inline images
|
||||
if (d.HTML && d.Attachments) {
|
||||
for (let i in d.Attachments) {
|
||||
let a = d.Attachments[i];
|
||||
if (a.ContentID != '') {
|
||||
d.HTML = d.HTML.replace(
|
||||
new RegExp('cid:'+a.ContentID, 'g'),
|
||||
window.location.origin+'/api/'+self.mailbox+'/'+d.ID+'/part/'+a.PartID
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.message = d;
|
||||
});
|
||||
},
|
||||
|
||||
deleteAll: function() {
|
||||
let self = this;
|
||||
let uri = 'api/' + self.mailbox + '/delete'
|
||||
self.get(uri, false, function(response) {
|
||||
self.reloadMessages();
|
||||
});
|
||||
},
|
||||
|
||||
deleteOne: function() {
|
||||
let self = this;
|
||||
if (!self.message) {
|
||||
return false;
|
||||
}
|
||||
let uri = 'api/' + self.mailbox + '/' + self.message.ID + '/delete'
|
||||
self.get(uri, false, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
markUnread: function() {
|
||||
let self = this;
|
||||
if (!self.message) {
|
||||
return false;
|
||||
}
|
||||
let uri = 'api/' + self.mailbox + '/' + self.message.ID + '/unread'
|
||||
self.get(uri, false, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
});
|
||||
},
|
||||
|
||||
// websocket connect
|
||||
connect: function () {
|
||||
let wsproto = location.protocol == 'https:' ? 'wss' : 'ws';
|
||||
let ws = new WebSocket(wsproto + "://" + document.location.host + "/api/"+this.mailbox+"/events");
|
||||
let self = this;
|
||||
ws.onmessage = function (e) {
|
||||
let response = JSON.parse(e.data);
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
// new messages
|
||||
if (response.Type == "new" && response.Data) {
|
||||
if (self.start < 1) {
|
||||
if (!self.searching) {
|
||||
self.items.unshift(response.Data);
|
||||
if (self.items.length > self.limit) {
|
||||
self.items.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.total++;
|
||||
} else if (response.Type == "prune") {
|
||||
// messages have been deleted, reload messages to adjust
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws.onopen = function () {
|
||||
self.isConnected = true;
|
||||
self.loadMessages();
|
||||
}
|
||||
|
||||
ws.onclose = function (e) {
|
||||
self.isConnected = false;
|
||||
|
||||
setTimeout(function () {
|
||||
self.connect(); // reconnect
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
ws.onerror = function (err) {
|
||||
ws.close();
|
||||
}
|
||||
},
|
||||
|
||||
getPrimaryEmailTo: function(message) {
|
||||
for (let i in message.To) {
|
||||
return message.To[i].Address;
|
||||
}
|
||||
|
||||
return '[ Unknown ]';
|
||||
},
|
||||
|
||||
getRelativeCreated: function(message) {
|
||||
let d = new Date(message.Created)
|
||||
return moment(d).fromNow().toString();
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar navbar-expand-lg navbar-light row flex-shrink-0 bg-light">
|
||||
<div class="col-lg-2 col-md-3 col-auto">
|
||||
<a class="navbar-brand" href="#" v-on:click="reloadMessages">
|
||||
<img src="mailpit.svg" alt="Mailpit">
|
||||
<span class="d-none d-md-inline-block ms-2">Mailpit</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-9 col-lg-8" v-if="message">
|
||||
<a class="btn btn-outline-secondary me-4 px-3" href="#" v-on:click="message=false" title="Return to messages">
|
||||
<i class="bi bi-arrow-return-left"></i>
|
||||
</a>
|
||||
<button class="btn btn-outline-secondary me-2" title="Delete message" v-on:click="deleteOne">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary me-2" title="Mark unread" v-on:click="markUnread">
|
||||
<i class="bi bi-envelope"></i>
|
||||
</button>
|
||||
<a :href="'api/' + mailbox + '/' + message.ID + '/source?dl=1'" class="btn btn-outline-secondary me-2" title="Download message">
|
||||
<i class="bi bi-file-arrow-down-fill"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-9 col-lg-5" v-if="!message && total">
|
||||
<form v-on:submit="doSearch">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" v-model.trim="search" placeholder="Search mailbox">
|
||||
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5 text-end" v-if="!message && total">
|
||||
<select v-model="limit" v-on:change="loadMessages"
|
||||
class="form-select form-select-sm d-inline w-auto me-1" v-if="!searching">
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="200">200</option>
|
||||
</select>
|
||||
<span v-if="searching">
|
||||
<b>{{ formatNumber(items.length) }} results</b>
|
||||
</span>
|
||||
<span v-else>
|
||||
<small>
|
||||
<b>{{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }}</b> of <b>{{ formatNumber(total) }}</b>
|
||||
</small>
|
||||
<button class="btn btn-outline-secondary ms-3 me-1" :disabled="!canPrev" v-on:click="viewPrev"
|
||||
v-if="!searching">
|
||||
<i class="bi bi-caret-left-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" :disabled="!canNext" v-on:click="viewNext" v-if="!searching">
|
||||
<i class="bi bi-caret-right-fill"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-fill" style="min-height:0">
|
||||
<div class="d-none d-md-block col-lg-2 col-md-3 mh-100 position-relative" style="overflow-y: auto;">
|
||||
<ul class="list-unstyled mt-3">
|
||||
<li v-if="isConnected" title="Messages will auto-load">
|
||||
<i class="bi bi-power text-success"></i>
|
||||
Connected
|
||||
</li>
|
||||
<li v-else title="Messages will auto-load">
|
||||
<i class="bi bi-power text-danger"></i>
|
||||
Disconnected
|
||||
</li>
|
||||
<li class="mt-3">
|
||||
<a class="position-relative ps-0" href="#" v-on:click="reloadMessages">
|
||||
<i class="bi bi-envelope me-1" v-if="isConnected"></i>
|
||||
<i class="bi bi-arrow-clockwise me-1" v-else></i>
|
||||
Inbox
|
||||
<span class="position-absolute mt-2 ms-4 start-100 translate-middle badge rounded-pill text-bg-secondary" v-if="total">
|
||||
{{ formatNumber(total) }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mt-3 mb-5">
|
||||
<a v-if="total" href="#" data-bs-toggle="modal" data-bs-target="#deleteAllModal">
|
||||
<i class="bi bi-trash-fill me-1 text-danger"></i>
|
||||
Delete all
|
||||
</a>
|
||||
</li>
|
||||
<li class="mt-5 position-fixed bottom-0 w-100">
|
||||
<a href="https://github.com/axllent/mailpit" target="_blank" class="text-muted w-100 d-block bg-white py-2">
|
||||
<i class="bi bi-github"></i>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-10 col-md-9 mh-100 pe-0">
|
||||
<div class="mh-100" style="overflow-y: auto;" :class="message ? 'd-none':''" id="message-page">
|
||||
<div class="list-group" v-if="items.length">
|
||||
<a v-for="message in items" :href="'#'+message.ID" class="row message d-flex small list-group-item list-group-item-action"
|
||||
:class="message.Read ? 'read':''" XXXv-on:click="openMessage(message)">
|
||||
<div class="col-md-3">
|
||||
<div class="d-md-none float-end text-muted text-nowrap small">
|
||||
<i class="bi bi-paperclip h6 me-1" v-if="message.Attachments"></i>
|
||||
{{ getRelativeCreated(message) }}
|
||||
</div>
|
||||
|
||||
<div class="text-truncate d-md-none">
|
||||
<span v-if="message.From" :title="message.From.Address">{{ message.From.Name ? message.From.Name : message.From.Address }}</span>
|
||||
</div>
|
||||
<div class="text-truncate d-none d-md-block">
|
||||
<b v-if="message.From" :title="message.From.Address">{{ message.From.Name ? message.From.Name : message.From.Address }}</b>
|
||||
</div>
|
||||
<div class="d-none d-md-block text-truncate text-muted small">
|
||||
{{ getPrimaryEmailTo(message) }}
|
||||
<span v-if="message.To && message.To.length > 1">
|
||||
[+{{message.To.length - 1}}]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mt-2 mt-md-0">
|
||||
<b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b>
|
||||
</div>
|
||||
<div class="d-none d-md-block col-1 small text-end text-muted">
|
||||
<i class="bi bi-paperclip float-start h6" v-if="message.Attachments"></i>
|
||||
{{ getFileSize(message.Size) }}
|
||||
</div>
|
||||
<div class="d-none d-md-block col-2 small text-end text-muted">
|
||||
{{ getRelativeCreated(message) }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else class="text-muted py-3">No messages</div>
|
||||
</div>
|
||||
|
||||
<Message v-if="message" :message="message" :mailbox="mailbox"></Message>
|
||||
</div>
|
||||
<div id="loading" v-if="loading">
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="deleteAllModal" tabindex="-1" aria-labelledby="deleteAllModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteAllModalLabel">Delete all messages?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
This will permanently delete all messages.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" v-on:click="deleteAll">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
8
server/ui-src/app.js
Normal file
8
server/ui-src/app.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import "./assets/bootstrap.scss";
|
||||
import "./assets/styles.scss";
|
||||
import "../../node_modules/bootstrap-icons/font/bootstrap-icons.scss";
|
||||
import "bootstrap";
|
||||
|
||||
createApp(App).mount('#app')
|
1
server/ui-src/assets/_bootstrap_variables.scss
Normal file
1
server/ui-src/assets/_bootstrap_variables.scss
Normal file
@ -0,0 +1 @@
|
||||
$link-decoration: none;
|
49
server/ui-src/assets/bootstrap.scss
vendored
Normal file
49
server/ui-src/assets/bootstrap.scss
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
@import "_bootstrap_variables";
|
||||
|
||||
// scss-docs-start import-stack
|
||||
// Configuration
|
||||
@import "../../../node_modules/bootstrap/scss/functions";
|
||||
@import "../../../node_modules/bootstrap/scss/variables";
|
||||
@import "../../../node_modules/bootstrap/scss/maps";
|
||||
@import "../../../node_modules/bootstrap/scss/mixins";
|
||||
@import "../../../node_modules/bootstrap/scss/utilities";
|
||||
|
||||
// Layout & components
|
||||
@import "../../../node_modules/bootstrap/scss/root";
|
||||
@import "../../../node_modules/bootstrap/scss/reboot";
|
||||
@import "../../../node_modules/bootstrap/scss/type";
|
||||
@import "../../../node_modules/bootstrap/scss/images";
|
||||
@import "../../../node_modules/bootstrap/scss/containers";
|
||||
@import "../../../node_modules/bootstrap/scss/grid";
|
||||
// @import "../../../node_modules/bootstrap/scss/tables";
|
||||
@import "../../../node_modules/bootstrap/scss/forms";
|
||||
@import "../../../node_modules/bootstrap/scss/buttons";
|
||||
// @import "../../../node_modules/bootstrap/scss/transitions";
|
||||
@import "../../../node_modules/bootstrap/scss/dropdown";
|
||||
@import "../../../node_modules/bootstrap/scss/button-group";
|
||||
@import "../../../node_modules/bootstrap/scss/nav";
|
||||
@import "../../../node_modules/bootstrap/scss/navbar";
|
||||
@import "../../../node_modules/bootstrap/scss/card";
|
||||
// @import "../../../node_modules/bootstrap/scss/accordion";
|
||||
// @import "../../../node_modules/bootstrap/scss/breadcrumb";
|
||||
// @import "../../../node_modules/bootstrap/scss/pagination";
|
||||
@import "../../../node_modules/bootstrap/scss/badge";
|
||||
// @import "../../../node_modules/bootstrap/scss/alert";
|
||||
// @import "../../../node_modules/bootstrap/scss/progress";
|
||||
@import "../../../node_modules/bootstrap/scss/list-group";
|
||||
@import "../../../node_modules/bootstrap/scss/close";
|
||||
// @import "../../../node_modules/bootstrap/scss/toasts";
|
||||
@import "../../../node_modules/bootstrap/scss/modal";
|
||||
// @import "../../../node_modules/bootstrap/scss/tooltip";
|
||||
// @import "../../../node_modules/bootstrap/scss/popover";
|
||||
// @import "../../../node_modules/bootstrap/scss/carousel";
|
||||
@import "../../../node_modules/bootstrap/scss/spinners";
|
||||
// @import "../../../node_modules/bootstrap/scss/offcanvas";
|
||||
// @import "../../../node_modules/bootstrap/scss/popover";
|
||||
|
||||
// Helpers
|
||||
@import "../../../node_modules/bootstrap/scss/helpers";
|
||||
|
||||
// Utilities
|
||||
@import "../../../node_modules/bootstrap/scss/utilities/api";
|
||||
// scss-docs-end import-stack
|
54
server/ui-src/assets/styles.scss
Normal file
54
server/ui-src/assets/styles.scss
Normal file
@ -0,0 +1,54 @@
|
||||
// @import "../../../node_modules/bootstrap-icons"; ///scss/root";
|
||||
|
||||
@import "bootstrap";
|
||||
|
||||
[v-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: #2d4a5d;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
z-index: 1500;
|
||||
}
|
||||
|
||||
.message.read:not(.active) {
|
||||
// background: $gray-100;
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
#nav-plain-text,
|
||||
#nav-source {
|
||||
white-space: pre;
|
||||
font-family: Courier New, Courier, System, fixed-width;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#nav-plain-text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.messageHeaders {
|
||||
margin: 15px 0 0;
|
||||
|
||||
th {
|
||||
padding-right: 10px;
|
||||
font-weight: normal;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
139
server/ui-src/mixins.js
Normal file
139
server/ui-src/mixins.js
Normal file
@ -0,0 +1,139 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// FakeModal is used to return a fake Bootstrap modal
|
||||
// if the ID returns nothing
|
||||
function FakeModal() { }
|
||||
FakeModal.prototype.hide = function () { alert('close fake modal') }
|
||||
FakeModal.prototype.show = function () { alert('open fake modal') }
|
||||
|
||||
/* Common mixin functions used in apps */
|
||||
const commonMixins = {
|
||||
data() {
|
||||
return {
|
||||
loading: 0,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getFileSize: function (bytes) {
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
|
||||
},
|
||||
|
||||
formatNumber: function (nr) {
|
||||
return new Intl.NumberFormat().format(nr);
|
||||
},
|
||||
|
||||
// Ajax error message
|
||||
handleError: function (error) {
|
||||
// handle error
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (error.response.data.Error) {
|
||||
alert(error.response.data.Error)
|
||||
} else {
|
||||
alert(error.response.data);
|
||||
}
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
alert('Error sending data to the server. Please try again.');
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
alert(error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// generic modal get/set function
|
||||
modal: function (id) {
|
||||
let e = document.getElementById(id);
|
||||
if (e) {
|
||||
return bootstrap.Modal.getOrCreateInstance(e);
|
||||
}
|
||||
// in case there are open/close actions
|
||||
return new FakeModal();
|
||||
},
|
||||
|
||||
// generic modal get/set function
|
||||
offcanvas: function (id) {
|
||||
var e = document.getElementById(id);
|
||||
if (e) {
|
||||
return bootstrap.Offcanvas.getOrCreateInstance(e);
|
||||
}
|
||||
// in case there are open/close actions
|
||||
return new FakeModal();
|
||||
},
|
||||
|
||||
/**
|
||||
* Axios GET request
|
||||
*
|
||||
* @params string url
|
||||
* @params array array parameters Object/array
|
||||
* @params function callback function
|
||||
*/
|
||||
get: function (url, values, callback) {
|
||||
let self = this;
|
||||
self.loading++;
|
||||
axios.get(url, { params: values })
|
||||
.then(callback)
|
||||
.catch(self.handleError)
|
||||
.then(function () {
|
||||
// always executed
|
||||
if (self.loading > 0) {
|
||||
self.loading--;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Axios Post request
|
||||
*
|
||||
* @params string url
|
||||
* @params array array parameters Object/array
|
||||
* @params function callback function
|
||||
*/
|
||||
post: function (url, values, callback) {
|
||||
let self = this;
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
params.append(key, value);
|
||||
}
|
||||
self.loading++;
|
||||
axios.post(url, params)
|
||||
.then(callback)
|
||||
.catch(self.handleError)
|
||||
.then(function () {
|
||||
// always executed
|
||||
if (self.loading > 0) {
|
||||
self.loading--;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Axios DELETE request (REST only)
|
||||
*
|
||||
* @params string url
|
||||
* @params array array parameters Object/array
|
||||
* @params function callback function
|
||||
*/
|
||||
delete: function (url, values, callback) {
|
||||
let self = this;
|
||||
self.loading++;
|
||||
axios.delete(url, { data: values })
|
||||
.then(callback)
|
||||
.catch(self.handleError)
|
||||
.then(function () {
|
||||
// always executed
|
||||
if (self.loading > 0) {
|
||||
self.loading--;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default commonMixins
|
173
server/ui-src/templates/Message.vue
Normal file
173
server/ui-src/templates/Message.vue
Normal file
@ -0,0 +1,173 @@
|
||||
|
||||
<script>
|
||||
import commonMixins from '../mixins.js';
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: Object,
|
||||
mailbox: Object,
|
||||
},
|
||||
mixins: [commonMixins],
|
||||
data() {
|
||||
return {
|
||||
srcURI: false,
|
||||
iframes: [], // for resizing
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var self = this;
|
||||
|
||||
window.addEventListener("resize", self.resizeIframes);
|
||||
|
||||
// click the first non-disabled tab
|
||||
document.querySelector('#nav-tab button:not([disabled])').click();
|
||||
document.activeElement.blur(); // blur focus
|
||||
|
||||
window.setTimeout(function(){
|
||||
let p = document.getElementById('preview-html');
|
||||
|
||||
if (p) {
|
||||
// make links open in new window
|
||||
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');
|
||||
|
||||
if (href && href.match(/^http/)) {
|
||||
anchorEl.setAttribute('target', '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
|
||||
var tabEl = document.getElementById('nav-source-tab');
|
||||
tabEl.addEventListener('shown.bs.tab', function (event) {
|
||||
self.srcURI = 'api/' + self.mailbox + '/' + self.message.ID + '/source';
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
resizeIframe: function(el) {
|
||||
let i = el.target;
|
||||
i.style.height = i.contentWindow.document.body.scrollHeight + 50 + 'px';
|
||||
},
|
||||
allAttachments: function(message){
|
||||
let a = [];
|
||||
for (let i in message.Attachments) {
|
||||
a.push(message.Attachments[i]);
|
||||
}
|
||||
for (let i in message.OtherParts) {
|
||||
a.push(message.OtherParts[i]);
|
||||
}
|
||||
for (let i in message.Inline) {
|
||||
a.push(message.Inline[i]);
|
||||
}
|
||||
|
||||
return a.length ? a : false;
|
||||
},
|
||||
messageDate: function(d) {
|
||||
return moment(d).format('ddd, D MMM YYYY, h:mm a');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="message" class="mh-100" style="overflow-y: scroll;">
|
||||
<table class="messageHeaders">
|
||||
<tbody>
|
||||
<tr class="small">
|
||||
<th>From</th>
|
||||
<td>
|
||||
<span v-if="message.From">
|
||||
{{ message.From.Name + " <" + message.From.Address +">" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
[ Unknown ]
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="small">
|
||||
<th>To</th>
|
||||
<td>
|
||||
<span v-for="(t, i) in message.To">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address +">" }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.Cc" class="small">
|
||||
<th>CC</th>
|
||||
<td>
|
||||
<span v-for="(t, i) in message.Cc">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address +">" }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.Bcc" class="small">
|
||||
<th>CC</th>
|
||||
<td>
|
||||
<span v-for="(t, i) in message.Bcc">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address +">" }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="small">Subject</th>
|
||||
<td><strong>{{ message.Subject }}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<nav>
|
||||
<div class="nav nav-tabs my-3" id="nav-tab" role="tablist">
|
||||
<button class="nav-link" id="nav-html-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#nav-html" type="button" role="tab" aria-controls="nav-html"
|
||||
aria-selected="true" :disabled="message.HTML == ''" :class="message.HTML == '' ? 'disabled':''">HTML</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':''">Plain<span class="d-none d-md-inline"> text</span></button>
|
||||
<button class="nav-link" id="nav-source-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#nav-source" type="button" role="tab" aria-controls="nav-source"
|
||||
aria-selected="false">Source</button>
|
||||
<button class="nav-link" id="nav-mime-tab" data-bs-toggle="tab" data-bs-target="#nav-mime"
|
||||
type="button" role="tab" aria-controls="nav-mime" aria-selected="false"
|
||||
:disabled="!allAttachments(message)" :class="!allAttachments(message) ? 'disabled':''"
|
||||
>Attachments <span v-if="allAttachments(message)">({{allAttachments(message).length}})</span></button>
|
||||
<div class="d-none d-lg-block ms-auto small mt-3 me-2 text-muted">
|
||||
<small>{{ messageDate(message.Date) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content mb-5" id="nav-tabContent">
|
||||
<div v-if="message.HTML != ''" class="tab-pane fade show" id="nav-html" role="tabpanel"
|
||||
aria-labelledby="nav-html-tab" tabindex="0">
|
||||
<iframe target-blank="" class="tab-pane" id="preview-html" :srcdoc="message.HTML" v-on:load="resizeIframe"
|
||||
seamless frameborder="0" style="width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel"
|
||||
aria-labelledby="nav-plain-text-tab" tabindex="0" :class="message.HTML == '' ? 'show':''">
|
||||
{{ message.Text }}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-source" role="tabpanel" aria-labelledby="nav-source-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>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-mime" role="tabpanel" aria-labelledby="nav-mime-tab"
|
||||
tabindex="0">
|
||||
<div v-if="allAttachments(message)" v-for="part in allAttachments(message)" class="mime-part mb-2">
|
||||
<a :href="'api/'+mailbox+'/'+message.ID+'/part/'+part.PartID" type="button"
|
||||
class="btn btn-outline-secondary btn-sm me-2" target="_blank">
|
||||
<i class="bi bi-file-arrow-down-fill"></i>
|
||||
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
|
||||
</a>
|
||||
<small class="text-muted">{{ getFileSize(part.Size) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
BIN
server/ui/favicon.ico
Normal file
BIN
server/ui/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
22
server/ui/index.html
Normal file
22
server/ui/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-100">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<link rel="icon" href="mailpit.svg">
|
||||
<title>Mailpit</title>
|
||||
<link rel=stylesheet href="dist/app.css">
|
||||
</head>
|
||||
|
||||
<body class="h-100">
|
||||
<div class="container-fluid h-100 d-flex flex-column" id="app">
|
||||
<noscript>You require JavaScript to use this app.</noscript>
|
||||
</div>
|
||||
|
||||
<script src="dist/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
97
server/ui/mailpit.svg
Normal file
97
server/ui/mailpit.svg
Normal file
@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="500"
|
||||
height="460"
|
||||
viewBox="0 0 132.29167 121.70833"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="mailpit.svg"
|
||||
inkscape:export-filename="/home/ralph/bitmap.png"
|
||||
inkscape:export-xdpi="176.09"
|
||||
inkscape:export-ydpi="176.09">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="90.98717"
|
||||
inkscape:cy="229.51456"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer2"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
units="px"
|
||||
inkscape:window-width="1548"
|
||||
inkscape:window-height="838"
|
||||
inkscape:window-x="52"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1">
|
||||
<sodipodi:guide
|
||||
position="39.014182,62.44412"
|
||||
orientation="0,1"
|
||||
id="guide4529"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 2"
|
||||
style="display:inline"
|
||||
transform="translate(-55.479864,-26.541592)">
|
||||
<g
|
||||
id="g4547"
|
||||
transform="matrix(1.9570423,0,0,1.9490788,-53.096581,-140.70068)"
|
||||
style="opacity:1">
|
||||
<path
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4534"
|
||||
d="M 61.775483,85.805801 89.296873,113.46893 116.98363,85.8058 Z"
|
||||
style="fill:#2d4a5f;fill-opacity:0.94117647;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4540"
|
||||
d="m 58.113837,90.436008 31.088544,30.616072 31.277529,-30.521576 -0.0945,18.898806 -30.71057,12.56771 7.748511,6.47285 -4.157737,3.07105 -21.26116,0.0945 c -2.471939,-0.0114 -13.222442,-9.40933 -13.890627,-21.16666 z"
|
||||
style="fill:#2d4a5f;fill-opacity:0.94117647;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccczzcccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4542"
|
||||
d="m 95.532643,122.7713 27.544977,-11.12272 -4.10354,29.40775 -6.05271,-4.68532 c -11.10189,11.88809 -23.124233,13.48775 -34.745034,10.69078 -11.620801,-2.79697 -16.420919,-10.7759 -20.062499,-18.2612 -3.64158,-7.4853 -2.976265,-15.74301 -1.181174,-23.10379 0.577547,5.393 -0.671158,8.37123 3.260045,17.24516 3.224283,5.84857 7.36483,10.47545 13.229166,12.80395 7.102803,3.17859 16.477397,1.7222 21.308409,-1.55916 l 7.276037,-6.2366 z"
|
||||
style="fill:#00b786;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
139
server/websockets/client.go
Normal file
139
server/websockets/client.go
Normal file
@ -0,0 +1,139 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Maximum message size allowed from peer.
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
var (
|
||||
newline = []byte{'\n'}
|
||||
space = []byte{' '}
|
||||
|
||||
// MessageHub global
|
||||
MessageHub *Hub
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true }, // allow multi-domain
|
||||
EnableCompression: true, // experimental compression
|
||||
}
|
||||
|
||||
// Client is a middleman between the websocket connection and the hub.
|
||||
type Client struct {
|
||||
hub *Hub
|
||||
|
||||
// The websocket connection.
|
||||
conn *websocket.Conn
|
||||
|
||||
// Buffered channel of outbound messages.
|
||||
send chan []byte
|
||||
}
|
||||
|
||||
// // readPump pumps messages from the websocket connection to the hub.
|
||||
// //
|
||||
// // The application runs readPump in a per-connection goroutine. The application
|
||||
// // ensures that there is at most one reader on a connection by executing all
|
||||
// // reads from this goroutine.
|
||||
// func (c *Client) readPump() {
|
||||
// defer func() {
|
||||
// c.hub.unregister <- c
|
||||
// c.conn.Close()
|
||||
// }()
|
||||
// c.conn.SetReadLimit(maxMessageSize)
|
||||
// c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
// c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
// for {
|
||||
// _, message, err := c.conn.ReadMessage()
|
||||
// if err != nil {
|
||||
// if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
// log.Printf("error: %v", err)
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
// message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
|
||||
// c.hub.Broadcast <- message
|
||||
// }
|
||||
// }
|
||||
|
||||
// writePump pumps messages from the hub to the websocket connection.
|
||||
//
|
||||
// A goroutine running writePump is started for each connection. The
|
||||
// application ensures that there is at most one writer to a connection by
|
||||
// executing all writes from this goroutine.
|
||||
func (c *Client) writePump() {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
_ = c.conn.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-c.send:
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if !ok {
|
||||
// The hub closed the channel.
|
||||
_ = c.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
|
||||
w, err := c.conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(message)
|
||||
|
||||
// Add queued chat messages to the current websocket message.
|
||||
n := len(c.send)
|
||||
for i := 0; i < n; i++ {
|
||||
_, _ = w.Write(newline)
|
||||
_, _ = w.Write(<-c.send)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
_ = c.conn.WriteMessage(websocket.PingMessage, []byte{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ServeWs handles websocket requests from the peer.
|
||||
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
|
||||
client.hub.register <- client
|
||||
|
||||
// Allow collection of memory referenced by the caller by doing all work in
|
||||
// new goroutines.
|
||||
go client.writePump()
|
||||
// go client.readPump()
|
||||
}
|
81
server/websockets/hub.go
Normal file
81
server/websockets/hub.go
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/axllent/mailpit/data"
|
||||
"github.com/axllent/mailpit/logger"
|
||||
)
|
||||
|
||||
// Hub maintains the set of active clients and broadcasts messages to the
|
||||
// clients.
|
||||
type Hub struct {
|
||||
// Registered clients.
|
||||
Clients map[*Client]bool
|
||||
|
||||
// Inbound messages from the clients.
|
||||
Broadcast chan []byte
|
||||
|
||||
// Register requests from the clients.
|
||||
register chan *Client
|
||||
|
||||
// Unregister requests from clients.
|
||||
unregister chan *Client
|
||||
}
|
||||
|
||||
// NewHub returns a new hub configuration
|
||||
func NewHub() *Hub {
|
||||
return &Hub{
|
||||
Broadcast: make(chan []byte),
|
||||
register: make(chan *Client),
|
||||
unregister: make(chan *Client),
|
||||
Clients: make(map[*Client]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the listener
|
||||
func (h *Hub) Run() {
|
||||
for {
|
||||
select {
|
||||
case client := <-h.register:
|
||||
h.Clients[client] = true
|
||||
case client := <-h.unregister:
|
||||
if _, ok := h.Clients[client]; ok {
|
||||
delete(h.Clients, client)
|
||||
close(client.send)
|
||||
}
|
||||
case message := <-h.Broadcast:
|
||||
logger.Log().Debugf("Message received: %s", message)
|
||||
for client := range h.Clients {
|
||||
select {
|
||||
case client.send <- message:
|
||||
default:
|
||||
close(client.send)
|
||||
delete(h.Clients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast will spawn a broadcast message to all connected clients
|
||||
func Broadcast(t string, msg interface{}) {
|
||||
if MessageHub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
w := data.WebsocketNotification{}
|
||||
w.Type = t
|
||||
w.Data = msg
|
||||
b, err := json.Marshal(w)
|
||||
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[http] broadcast received invalid data: %s", err)
|
||||
}
|
||||
|
||||
go func() { MessageHub.Broadcast <- b }()
|
||||
}
|
39
smtpd/smtpd.go
Normal file
39
smtpd/smtpd.go
Normal file
@ -0,0 +1,39 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/mail"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
s "github.com/mhale/smtpd"
|
||||
)
|
||||
|
||||
func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
logger.Log().Errorf("error parsing message: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := storage.Store(storage.DefaultMailbox, data); err != nil {
|
||||
logger.Log().Errorf("error storing message: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
subject := msg.Header.Get("Subject")
|
||||
logger.Log().Debugf("[smtp] received mail from %s for %s with subject %s", from, to[0], subject)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Listen starts the SMTPD server
|
||||
func Listen() error {
|
||||
logger.Log().Infof("[smtp] starting on %s", config.SMTPListen)
|
||||
if err := s.ListenAndServe(config.SMTPListen, mailHandler, "Mailpit", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
560
storage/database.go
Normal file
560
storage/database.go
Normal file
@ -0,0 +1,560 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/data"
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/ostafen/clover"
|
||||
)
|
||||
|
||||
var (
|
||||
db *clover.DB
|
||||
|
||||
// DefaultMailbox allowing for potential exampnsion in the future
|
||||
DefaultMailbox = "catchall"
|
||||
|
||||
count int
|
||||
per100start = time.Now()
|
||||
)
|
||||
|
||||
// CloverStore struct
|
||||
type CloverStore struct {
|
||||
Created time.Time
|
||||
Read bool
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
Subject string
|
||||
Size int
|
||||
Inline int
|
||||
Attachments int
|
||||
SearchText string
|
||||
}
|
||||
|
||||
// InitDB will initialise the database.
|
||||
// If config.DataDir is empty then it will be in memory.
|
||||
func InitDB() error {
|
||||
var err error
|
||||
if config.DataDir != "" {
|
||||
logger.Log().Infof("[db] initialising data storage: %s", config.DataDir)
|
||||
db, err = clover.Open(config.DataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
// catch all signals since not explicitly listing
|
||||
// Program that will listen to the SIGINT and SIGTERM
|
||||
// SIGINT will listen to CTRL-C.
|
||||
// SIGTERM will be caught if kill command executed
|
||||
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
|
||||
// method invoked upon seeing signal
|
||||
go func() {
|
||||
s := <-sigs
|
||||
logger.Log().Infof("[db] got %s signal, saving persistant data & shutting down", s)
|
||||
if err := db.Close(); err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
} else {
|
||||
logger.Log().Debug("[db] initialising memory data storage")
|
||||
db, err = clover.Open("", clover.InMemoryMode(true))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// auto-prune
|
||||
if config.MaxMessages > 0 {
|
||||
go pruneCron()
|
||||
}
|
||||
|
||||
// create catch-all collection
|
||||
return CreateMailbox(DefaultMailbox)
|
||||
}
|
||||
|
||||
// ListMailboxes returns a slice of mailboxes (collections)
|
||||
func ListMailboxes() ([]data.MailboxSummary, error) {
|
||||
mailboxes, err := db.ListCollections()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := []data.MailboxSummary{}
|
||||
|
||||
for _, m := range mailboxes {
|
||||
|
||||
total, err := Count(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unread, err := CountUnread(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mb := data.MailboxSummary{}
|
||||
mb.Name = m
|
||||
mb.Slug = m
|
||||
mb.Total = total
|
||||
mb.Unread = unread
|
||||
|
||||
if total > 0 {
|
||||
q, err := db.FindFirst(
|
||||
clover.NewQuery(m).Sort(clover.SortOption{Field: "Created", Direction: -1}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mb.LastMessage = q.Get("Created").(time.Time)
|
||||
}
|
||||
|
||||
results = append(results, mb)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// MailboxExists is used to return whether a collection (aka: mailbox) exists
|
||||
func MailboxExists(name string) bool {
|
||||
ok, err := db.HasCollection(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// CreateMailbox will create a collection if it does not exist
|
||||
func CreateMailbox(name string) error {
|
||||
if !MailboxExists(name) {
|
||||
logger.Log().Infof("[db] creating mailbox: %s", name)
|
||||
|
||||
if err := db.CreateCollection(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create Created index
|
||||
if err := db.CreateIndex(name, "Created"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create Read index
|
||||
if err := db.CreateIndex(name, "Read"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create separate collection for data
|
||||
if err := db.CreateCollection(name + "_data"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create Created index
|
||||
if err := db.CreateIndex(name+"_data", "Created"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store will store a message in the database and return the unique ID
|
||||
func Store(mailbox string, b []byte) (string, error) {
|
||||
r := bytes.NewReader(b)
|
||||
// Parse message body with enmime.
|
||||
env, err := enmime.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var from *mail.Address
|
||||
fromData := addressToSlice(env, "From")
|
||||
if len(fromData) > 0 {
|
||||
from = fromData[0]
|
||||
}
|
||||
|
||||
obj := CloverStore{
|
||||
Created: time.Now(),
|
||||
From: from,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Size: len(b),
|
||||
Inline: len(env.Inlines),
|
||||
Attachments: len(env.Attachments),
|
||||
SearchText: createSearchText(env),
|
||||
}
|
||||
|
||||
doc := clover.NewDocumentOf(obj)
|
||||
|
||||
id, err := db.InsertOne(mailbox, doc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// save the raw email in a separate collection
|
||||
raw := clover.NewDocument()
|
||||
raw.Set("_id", id)
|
||||
raw.Set("Created", time.Now())
|
||||
raw.Set("Data", string(b))
|
||||
_, err = db.InsertOne(mailbox+"_data", raw)
|
||||
if err != nil {
|
||||
// delete the summary because the data insert failed
|
||||
logger.Log().Debugf("[db] error inserting raw message, rolling back")
|
||||
_ = DeleteOneMessage(mailbox, id)
|
||||
return "", err
|
||||
}
|
||||
|
||||
count++
|
||||
if count%100 == 0 {
|
||||
logger.Log().Infof("%d messages added (%s per 100)", count, time.Since(per100start))
|
||||
|
||||
per100start = time.Now()
|
||||
}
|
||||
|
||||
d, err := db.FindById(DefaultMailbox, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c := &data.Summary{}
|
||||
if err := d.Unmarshal(c); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c.ID = id
|
||||
|
||||
websockets.Broadcast("new", c)
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// List returns a summary of messages.
|
||||
// For pertformance reasons we manually paginate over queries of 100 results
|
||||
// as clover's `Skip()` returns a subset of all results which is much slower.
|
||||
// @see https://github.com/ostafen/clover/issues/73
|
||||
func List(mailbox string, start, limit int) ([]data.Summary, error) {
|
||||
var lastDoc *clover.Document
|
||||
count := 0
|
||||
startAddingAt := start + 1
|
||||
adding := false
|
||||
results := []data.Summary{}
|
||||
|
||||
for {
|
||||
var instant time.Time
|
||||
if lastDoc == nil {
|
||||
instant = time.Now()
|
||||
} else {
|
||||
instant = lastDoc.Get("Created").(time.Time)
|
||||
}
|
||||
|
||||
all, err := db.FindAll(
|
||||
clover.NewQuery(mailbox).
|
||||
Where(clover.Field("Created").Lt(instant)).
|
||||
Sort(clover.SortOption{Field: "Created", Direction: -1}).
|
||||
Limit(100),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range all {
|
||||
count++
|
||||
|
||||
if count == startAddingAt {
|
||||
adding = true
|
||||
}
|
||||
|
||||
resultsLen := len(results)
|
||||
|
||||
if adding && resultsLen < limit {
|
||||
cs := &data.Summary{}
|
||||
if err := d.Unmarshal(cs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cs.ID = d.ObjectId()
|
||||
results = append(results, *cs)
|
||||
}
|
||||
}
|
||||
|
||||
// we have enough resuts
|
||||
if len(results) == limit {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
if len(all) > 0 {
|
||||
lastDoc = all[len(all)-1]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Search returns a summary of items mathing a search. It searched the SearchText field.
|
||||
func Search(mailbox, search string, start, limit int) ([]data.Summary, error) {
|
||||
sq := fmt.Sprintf("(?i)%s", regexp.QuoteMeta(search))
|
||||
q, err := db.FindAll(clover.NewQuery(mailbox).
|
||||
Skip(start).
|
||||
Limit(limit).
|
||||
Sort(clover.SortOption{Field: "Created", Direction: -1}).
|
||||
Where(clover.Field("SearchText").Like(sq)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := []data.Summary{}
|
||||
|
||||
for _, d := range q {
|
||||
cs := &CloverStore{}
|
||||
if err := d.Unmarshal(cs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results = append(results, cs.Summary(d.ObjectId()))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Count returns the total number of messages in a mailbox
|
||||
func Count(mailbox string) (int, error) {
|
||||
return db.Count(clover.NewQuery(mailbox))
|
||||
}
|
||||
|
||||
// CountUnread returns the unread number of messages in a mailbox
|
||||
func CountUnread(mailbox string) (int, error) {
|
||||
return db.Count(
|
||||
clover.NewQuery(mailbox).
|
||||
Where(clover.Field("Read").IsFalse()),
|
||||
)
|
||||
}
|
||||
|
||||
// Summary generated a message summary. ID must be supplied
|
||||
// as this is not stored within the CloverStore but rather the
|
||||
// *clover.Document
|
||||
func (c *CloverStore) Summary(id string) data.Summary {
|
||||
s := data.Summary{
|
||||
ID: id,
|
||||
From: c.From,
|
||||
To: c.To,
|
||||
Cc: c.Cc,
|
||||
Bcc: c.Bcc,
|
||||
Subject: c.Subject,
|
||||
Created: c.Created,
|
||||
Size: c.Size,
|
||||
Attachments: c.Attachments,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// GetMessage returns a data.Message generated from the {mailbox}_data collection.
|
||||
// ID must be supplied as this is not stored within the CloverStore but rather the
|
||||
// *clover.Document
|
||||
func GetMessage(mailbox, id string) (*data.Message, error) {
|
||||
q, err := db.FindById(mailbox+"_data", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if q == nil {
|
||||
return nil, errors.New("message not found")
|
||||
}
|
||||
|
||||
raw := q.Get("Data").(string)
|
||||
|
||||
r := bytes.NewReader([]byte(raw))
|
||||
|
||||
env, err := enmime.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var from *mail.Address
|
||||
fromData := addressToSlice(env, "From")
|
||||
if len(fromData) > 0 {
|
||||
from = fromData[0]
|
||||
}
|
||||
|
||||
date, err := env.Date()
|
||||
if err != nil {
|
||||
// date =
|
||||
}
|
||||
|
||||
obj := data.Message{
|
||||
ID: q.ObjectId(),
|
||||
Read: true,
|
||||
Created: q.Get("Created").(time.Time),
|
||||
From: from,
|
||||
Date: date,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Size: len(raw),
|
||||
Text: env.Text,
|
||||
}
|
||||
|
||||
html := env.HTML
|
||||
|
||||
// strip base tags
|
||||
var re = regexp.MustCompile(`(?U)<base .*>`)
|
||||
html = re.ReplaceAllString(html, "")
|
||||
|
||||
for _, i := range env.Inlines {
|
||||
if i.FileName != "" || i.ContentID != "" {
|
||||
obj.Inline = append(obj.Inline, data.AttachmentSummary(i))
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range env.OtherParts {
|
||||
if i.FileName != "" || i.ContentID != "" {
|
||||
obj.Inline = append(obj.Inline, data.AttachmentSummary(i))
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range env.Attachments {
|
||||
if a.FileName != "" || a.ContentID != "" {
|
||||
obj.Attachments = append(obj.Attachments, data.AttachmentSummary(a))
|
||||
}
|
||||
}
|
||||
|
||||
obj.HTML = html
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
updates["Read"] = true
|
||||
|
||||
if err := db.UpdateById(mailbox, id, updates); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &obj, nil
|
||||
}
|
||||
|
||||
// GetAttachmentPart returns an *enmime.Part (attachment or inline) from a message
|
||||
func GetAttachmentPart(mailbox, id, partID string) (*enmime.Part, error) {
|
||||
data, err := GetMessageRaw(mailbox, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
env, err := enmime.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, a := range env.Inlines {
|
||||
if a.PartID == partID {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range env.OtherParts {
|
||||
if a.PartID == partID {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range env.Attachments {
|
||||
if a.PartID == partID {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("attachment not found")
|
||||
}
|
||||
|
||||
// GetMessageRaw returns an []byte of the full message
|
||||
func GetMessageRaw(mailbox, id string) ([]byte, error) {
|
||||
q, err := db.FindById(mailbox+"_data", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if q == nil {
|
||||
return nil, errors.New("message not found")
|
||||
}
|
||||
|
||||
data := q.Get("Data").(string)
|
||||
|
||||
return []byte(data), err
|
||||
}
|
||||
|
||||
// UnreadMessage will delete all messages from a mailbox
|
||||
func UnreadMessage(mailbox, id string) error {
|
||||
updates := make(map[string]interface{})
|
||||
updates["Read"] = false
|
||||
|
||||
return db.UpdateById(mailbox, id, updates)
|
||||
}
|
||||
|
||||
// DeleteOneMessage will delete a single message from a mailbox
|
||||
func DeleteOneMessage(mailbox, id string) error {
|
||||
if err := db.DeleteById(mailbox, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.DeleteById(mailbox+"_data", id)
|
||||
}
|
||||
|
||||
// DeleteAllMessages will delete all messages from a mailbox
|
||||
func DeleteAllMessages(mailbox string) error {
|
||||
|
||||
totalStart := time.Now()
|
||||
|
||||
totalMessages, err := db.Count(clover.NewQuery(mailbox))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
toDelete, err := db.Count(clover.NewQuery(mailbox))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if toDelete == 0 {
|
||||
break
|
||||
}
|
||||
if err := db.Delete(clover.NewQuery(mailbox).Limit(2500)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Delete(clover.NewQuery(mailbox + "_data").Limit(2500)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if err := db.Delete(clover.NewQuery(mailbox)); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// if err := db.Delete(clover.NewQuery(mailbox + "_data")); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
elapsed := time.Since(totalStart)
|
||||
logger.Log().Infof("Deleted %d messages from %s in %s", totalMessages, mailbox, elapsed)
|
||||
|
||||
return nil
|
||||
}
|
180
storage/database_test.go
Normal file
180
storage/database_test.go
Normal file
@ -0,0 +1,180 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
)
|
||||
|
||||
var (
|
||||
testTextEmail []byte
|
||||
testMimeEmail []byte
|
||||
)
|
||||
|
||||
func TestTextEmailInserts(t *testing.T) {
|
||||
setup()
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < 1000; i++ {
|
||||
if _, err := Store(DefaultMailbox, testTextEmail); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
count, err := Count(DefaultMailbox)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, count, 1000, "incorrect number of text emails stored")
|
||||
|
||||
t.Logf("inserted 1,000 text emails in %s\n", time.Since(start))
|
||||
|
||||
delStart := time.Now()
|
||||
if err := DeleteAllMessages(DefaultMailbox); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
count, err = Count(DefaultMailbox)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, count, 0, "incorrect number of text emails deleted")
|
||||
|
||||
t.Logf("deleted 1,000 text emails in %s\n", time.Since(delStart))
|
||||
|
||||
db.Close()
|
||||
}
|
||||
|
||||
func TestMimeEmailInserts(t *testing.T) {
|
||||
setup()
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < 1000; i++ {
|
||||
if _, err := Store(DefaultMailbox, testMimeEmail); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
count, err := Count(DefaultMailbox)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, count, 1000, "incorrect number of mime emails stored")
|
||||
|
||||
t.Logf("inserted 1,000 emails with mime attachments in %s\n", time.Since(start))
|
||||
|
||||
delStart := time.Now()
|
||||
if err := DeleteAllMessages(DefaultMailbox); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
count, err = Count(DefaultMailbox)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, count, 0, "incorrect number of mime emails deleted")
|
||||
|
||||
t.Logf("deleted 1,000 mime emails in %s\n", time.Since(delStart))
|
||||
|
||||
db.Close()
|
||||
}
|
||||
|
||||
func TestRetrieveMimeEmail(t *testing.T) {
|
||||
setup()
|
||||
|
||||
id, err := Store(DefaultMailbox, testMimeEmail)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
msg, err := GetMessage(DefaultMailbox, id)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match")
|
||||
assertEqual(t, msg.From.Address, "sender@example.com", "\"From\" address does not match")
|
||||
assertEqual(t, msg.Subject, "inline + attachment", "subject does not match")
|
||||
assertEqual(t, len(msg.To), 1, "incorrect number of recipients")
|
||||
assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match")
|
||||
assertEqual(t, msg.To[0].Address, "recipient@example.com", "\"To\" address does not match")
|
||||
assertEqual(t, len(msg.Attachments), 1, "incorrect number of attachments")
|
||||
assertEqual(t, msg.Attachments[0].FileName, "Sample PDF.pdf", "attachment filename does not match")
|
||||
assertEqual(t, len(msg.Inline), 1, "incorrect number of inline attachments")
|
||||
assertEqual(t, msg.Inline[0].FileName, "inline-image.jpg", "inline attachment filename does not match")
|
||||
attachmentData, err := GetAttachmentPart(DefaultMailbox, id, msg.Attachments[0].PartID)
|
||||
assertEqual(t, len(attachmentData.Content), msg.Attachments[0].Size, "attachment size does not match")
|
||||
inlineData, err := GetAttachmentPart(DefaultMailbox, id, msg.Inline[0].PartID)
|
||||
assertEqual(t, len(inlineData.Content), msg.Inline[0].Size, "inline attachment size does not match")
|
||||
}
|
||||
|
||||
func BenchmarkImportText(b *testing.B) {
|
||||
setup()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := Store(DefaultMailbox, testTextEmail); err != nil {
|
||||
b.Log("error ", err)
|
||||
b.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
db.Close()
|
||||
}
|
||||
|
||||
func BenchmarkImportMime(b *testing.B) {
|
||||
setup()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := Store(DefaultMailbox, testMimeEmail); err != nil {
|
||||
b.Log("error ", err)
|
||||
b.Fail()
|
||||
}
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
|
||||
func setup() {
|
||||
config.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
if err := InitDB(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
testTextEmail, err = ioutil.ReadFile("testdata/plain-text.eml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
testMimeEmail, err = ioutil.ReadFile("testdata/mime-attachment.eml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
if a == b {
|
||||
return
|
||||
}
|
||||
message = fmt.Sprintf("%s: \"%v\" != \"%v\"", message, a, b)
|
||||
t.Fatal(message)
|
||||
}
|
607
storage/testdata/mime-attachment.eml
vendored
Normal file
607
storage/testdata/mime-attachment.eml
vendored
Normal file
@ -0,0 +1,607 @@
|
||||
Delivered-To: recipient@example.com
|
||||
Received: by 2002:a0c:fe87:0:0:0:0:0 with SMTP id d7csp145570qvs;
|
||||
Tue, 26 Jul 2022 20:42:36 -0700 (PDT)
|
||||
X-Received: by 2002:a17:902:f788:b0:16c:f48b:905e with SMTP id q8-20020a170902f78800b0016cf48b905emr19885972pln.60.1658893355881;
|
||||
Tue, 26 Jul 2022 20:42:35 -0700 (PDT)
|
||||
ARC-Seal: i=1; a=rsa-sha256; t=1658893355; cv=none;
|
||||
d=google.com; s=arc-20160816;
|
||||
b=WkNqsJS6Q7RhLY79RZAXgq+Moe0ZcMpGfkZMPq+v1YvG9yAao+QVeY+lN0vjM27H39
|
||||
0QcXaTd4me7k0f96We657eNyjXSVaJyvvEYMA/Eu/bM51DrzsqywIfMq/O/xsA64mHph
|
||||
o8LBjV3YjjfNY1uN3q/eLLd5ZLEiHulQSyKJwXxPs7FXaCiihK1iys4U/wEcVubANo0K
|
||||
3DLhQ2NYrFOjN4jEyw8Agv3PjmLwgAFFisjt49Zm0N6sIDjgWLncXPQ0dA7MjKKE6pjQ
|
||||
terzh43sjNeI6O+WQJ+aZ6nDxLzhgc+tk0sa290o4u7mjH8/qRx8/krqSPPlgGjLbdyo
|
||||
Utlg==
|
||||
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
|
||||
h=subject:from:to:content-language:user-agent:mime-version:date
|
||||
:message-id:dkim-signature;
|
||||
bh=ofRVIgn/FP/zLSWstZbrzzKm87NpwtZOSgBYqfbXIl0=;
|
||||
b=hnIfinlV6u631zlofA336KWFWzAQrScmCiXIxlBoBrZfgy0FsVJ07tRXSzqqeofkHU
|
||||
k9pEKVJtD0FfkKzVdrAjetlBAbWCmbQf2u0AzaWqYVLk1rGSQj+UdpuIzMSuB5tX6sX5
|
||||
XgGvkQC6cYoSd/pRGcxmrA6+jnW531pGvaQzxyv3rpcnYrOT+LBgxaaFVn3fEeUC+AWs
|
||||
ZQHfciTV9hRCrmu2JWo47Z8RDr9SV3TLU/Mbf8G/p+PiaxhfxYarcTEoiV8+PuD9g6Et
|
||||
tm1PAqdGq7NAWezv943ueamREZHWiD9+h1gSOro/BmdpWmigEhKFovxRlbAzwsZtW7xo
|
||||
uSfA==
|
||||
ARC-Authentication-Results: i=1; mx.google.com;
|
||||
dkim=pass header.i=@gmail.com header.s=20210112 header.b=mywi6bMa;
|
||||
spf=pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=sender@example.com;
|
||||
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com
|
||||
Return-Path: <sender@example.com>
|
||||
Received: from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.85.220.41])
|
||||
by mx.google.com with SMTPS id 11-20020aa7914b000000b0052ab192de4fsor8543241pfi.101.2022.07.26.20.42.35
|
||||
for <recipient@example.com>
|
||||
(Google Transport Security);
|
||||
Tue, 26 Jul 2022 20:42:35 -0700 (PDT)
|
||||
Received-SPF: pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) client-ip=209.85.220.41;
|
||||
Authentication-Results: mx.google.com;
|
||||
dkim=pass header.i=@gmail.com header.s=20210112 header.b=mywi6bMa;
|
||||
spf=pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=sender@example.com;
|
||||
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=gmail.com; s=20210112;
|
||||
h=message-id:date:mime-version:user-agent:content-language:to:from
|
||||
:subject;
|
||||
bh=ofRVIgn/FP/zLSWstZbrzzKm87NpwtZOSgBYqfbXIl0=;
|
||||
b=mywi6bMa68lM9RENvBG2mjVlMvGhyZCrh3z9gE57KY0ZK0RLLPFxzAVOXtJpaTGQ0M
|
||||
C4W33O+7h5cvgFkLQJHc5YCemxEjCE5Dz5/uH4iSBYowkvn7Gu4TudNZtkNw8TGxH/Lf
|
||||
lKJiaqtdnm8YdLWCzG1M/scBbbjZxDrTLddshu/Q1ireNliVwl9WdN25zXQLxsEqHFXc
|
||||
5rVjyruB7cnshL8m14LYi+m5iN3H+o42oGzVce3+wQ31s+Bo/LBezb0qD8TRfTjnhp8u
|
||||
77RU61IOSMbuwQWNQCywxCnoZolZpR9qRgzd5rg73dGpXHIyNfBsYyb5vr28+fp93Ayo
|
||||
LXyw==
|
||||
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=1e100.net; s=20210112;
|
||||
h=x-gm-message-state:message-id:date:mime-version:user-agent
|
||||
:content-language:to:from:subject;
|
||||
bh=ofRVIgn/FP/zLSWstZbrzzKm87NpwtZOSgBYqfbXIl0=;
|
||||
b=BvDmv5WC7f4WSVvuypzr9WNT7AUCQeEexvjmGur1rfkZcmqr62punbNEvcyk6T5Iy7
|
||||
8XstlNbijtU9zT3qm5LBTEw1e7q8VACWVVHUbI5uE4NhqXbY6vfN4bxrDzRO/P+Ntr90
|
||||
BwH1dYSBLpYOmFGX6GlrOCg0X1MZgzGI92YakpQitGBjhKnWvvQ4NlX7Ivk6W6W2aHt5
|
||||
xkIVmZNdC13evcdFUOrQxcfFAkIe3kSR8eGVt++yoHlCt/fFv/QQjf5L9fEbteuA8h2V
|
||||
pnfH4fN5z+GF3rpeSl1VebfW8NtPy/iHAze6dlodAVM0jtaom8MtHSXfquCea/2giq0o
|
||||
YXQQ==
|
||||
X-Gm-Message-State: AJIora/WUqr3biShTHQBjSlCKazFbrLxeYpxmr1VF0TpBUbjnJrcLT77
|
||||
pdFYYiNICxragxqhNqXvw7/elR8u6B8=
|
||||
X-Google-Smtp-Source: AGRyM1tai6X1Bx130Y1yHG5w2e0r8wx6bbI+H+YppWmQoT28TV3dSoYCqmeQK5VViW8WuvdOpQzhPQ==
|
||||
X-Received: by 2002:a62:29c3:0:b0:52b:f774:7242 with SMTP id p186-20020a6229c3000000b0052bf7747242mr12504553pfp.67.1658893354675;
|
||||
Tue, 26 Jul 2022 20:42:34 -0700 (PDT)
|
||||
Return-Path: <sender@example.com>
|
||||
Received: from [192.168.1.2] ([8.8.8.8])
|
||||
by smtp.gmail.com with ESMTPSA id oj16-20020a17090b4d9000b001f291c9d3bdsm387578pjb.48.2022.07.26.20.42.32
|
||||
for <recipient@example.com>
|
||||
(version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128);
|
||||
Tue, 26 Jul 2022 20:42:33 -0700 (PDT)
|
||||
Content-Type: multipart/mixed; boundary="------------ae0qIOkrNQLQHe1YyfTsUXrk"
|
||||
Message-ID: <33af2ac1-c33d-9738-35e3-a6daf90bbd89@gmail.com>
|
||||
Date: Wed, 27 Jul 2022 15:42:29 +1200
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101
|
||||
Thunderbird/91.11.0
|
||||
Content-Language: en-NZ
|
||||
To: "Recipient Ross" <recipient@example.com>
|
||||
From: Sender Smith <sender@example.com>
|
||||
Subject: inline + attachment
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------ae0qIOkrNQLQHe1YyfTsUXrk
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="------------GGc8vauWscgVN0JHIav4AOeV"
|
||||
|
||||
--------------GGc8vauWscgVN0JHIav4AOeV
|
||||
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Message with inline image and attachment:
|
||||
|
||||
|
||||
|
||||
|
||||
--------------GGc8vauWscgVN0JHIav4AOeV
|
||||
Content-Type: multipart/related;
|
||||
boundary="------------z0ttbxz8BplvjsfeE7Zogcgs"
|
||||
|
||||
--------------z0ttbxz8BplvjsfeE7Zogcgs
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
Message with inline image and attachment:<br>
|
||||
<br>
|
||||
<img src="cid:part1.845LaYlX.wtWMpWwa@gmail.com"
|
||||
moz-do-not-send="false"><br>
|
||||
<br>
|
||||
<br>
|
||||
</body>
|
||||
</html>
|
||||
--------------z0ttbxz8BplvjsfeE7Zogcgs
|
||||
Content-Type: image/jpeg; name="inline-image.jpg"
|
||||
Content-Disposition: inline; filename="inline-image.jpg"
|
||||
Content-Id: <part1.845LaYlX.wtWMpWwa@gmail.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
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
|
||||
P//Z
|
||||
|
||||
--------------z0ttbxz8BplvjsfeE7Zogcgs--
|
||||
|
||||
--------------GGc8vauWscgVN0JHIav4AOeV--
|
||||
--------------ae0qIOkrNQLQHe1YyfTsUXrk
|
||||
Content-Type: application/pdf; name="Sample PDF.pdf"
|
||||
Content-Disposition: attachment; filename="Sample PDF.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
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
|
||||
/w67cv9U2GGHr1K4z4B7hwqUew0YKoB78NA9Q/CVuwuVr8yGuwvhrmPw5TufUb5swJ3b1yp3
|
||||
PgN3bpG3/1mlsn0tbHfLf1YJdxhw+5eqldsN+FI13IZi3uaBW2/JV24thlvyYRsubAvBzaip
|
||||
mythqwP+1IAvfsGhfNGALzjgJgO2GHCjAe6TN2zerNxgwObNcH0INgWcyqZKuM6AjQZcWwiD
|
||||
42GAQr8BiWMQPwaxY3DNMYgaoBsQMaC3HDYYsN5Rr6zvgLABPZuhGyddBmgGhAwIGtBpgLoI
|
||||
1h2DPxkPaw34rAFXG7BmNVXWHIPVFFZNmqqsqoOVBqxAyivqIeCEDsmudEwBfzFcdeVE5SoD
|
||||
2vOhzYDW5Xal1YDldmgxYBnuLDPgSp9duXIi+EoKFJ8dlhZAswFNQ+AdgkYDGmxzlIZjUP8M
|
||||
eJaB24AlBlzxmSLlimL4zOIJymeKYPHlBcpi98kJcHkBLDJgoQGXLShWLjsGC+bblQXFMH9e
|
||||
vjLfDvPy4dJSmFsAdZ/OV+oM+HQ+1NbkK7UFUJMP1XPGKdV2mDMOXHVwyacqlUtC8KmqIuVT
|
||||
lVBVBLNnVSqzPTCrEi6uzFcungCV+TDTgBkGVEyAcpSzvAhYCMqOQSmKUBqCkgKYjhqcbsC0
|
||||
Y3BRPUzFyVQDpoRgMmpqsgGT8NCkqeA0oNiAiQYUIUCRAQ6U1VEP9s0wIQSFBhSMn6QUGDAe
|
||||
ocdPgnwDqB3GGZCHYHkG5BZDTghk3JTRA5yAq2CADee2OSDZgRggDUuhrXdIl/xv+CEfNwNn
|
||||
/Cn5H+T5xf0KZW5kc3RyZWFtCmVuZG9iagoKNiAwIG9iago3MjA5CmVuZG9iagoKNyAwIG9i
|
||||
ago8PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnROYW1lL0JBQUFBQStEZWphVnVTYW5zCi9G
|
||||
bGFncyA0Ci9Gb250QkJveFstMTAyMCAtNDYyIDE3OTIgMTIzMl0vSXRhbGljQW5nbGUgMAov
|
||||
QXNjZW50IDkyOAovRGVzY2VudCAtMjM1Ci9DYXBIZWlnaHQgMTIzMgovU3RlbVYgODAKL0Zv
|
||||
bnRGaWxlMiA1IDAgUgo+PgplbmRvYmoKCjggMCBvYmoKPDwvTGVuZ3RoIDI2NS9GaWx0ZXIv
|
||||
RmxhdGVEZWNvZGU+PgpzdHJlYW0KeJxdkE1uwyAQhfecgmW6iMCO7TSSZalyFMmLtFXdHgDD
|
||||
2EGKAWG88O3LT9pKXYC+YeYN84a03blT0pF3q3kPDo9SCQuLXi0HPMAkFcpyLCR3jyjefGYG
|
||||
Ea/tt8XB3KlR1zUiHz63OLvh3YvQAzwh8mYFWKkmvPtqex/3qzF3mEE5TFHTYAGj73Nl5pXN
|
||||
QKJq3wmflm7be8lfwedmAOcxztIoXAtYDONgmZoA1ZQ2uL5cGgRK/MudkmIY+Y1ZX5n5SkrL
|
||||
Q+M5j1xlgQ+Jz4GLyEcauEzvbeAqcRn4mPrEmufIRRH4lLiKszx+DVOFtf24xXy11juNu40W
|
||||
gzmp4Hf9Rpugiucbox+AAgplbmRzdHJlYW0KZW5kb2JqCgo5IDAgb2JqCjw8L1R5cGUvRm9u
|
||||
dC9TdWJ0eXBlL1RydWVUeXBlL0Jhc2VGb250L0JBQUFBQStEZWphVnVTYW5zCi9GaXJzdENo
|
||||
YXIgMAovTGFzdENoYXIgOQovV2lkdGhzWzYwMCA2MzQgNjEyIDk3NCA2MzQgMjc3IDYxNSA2
|
||||
MDMgNzcwIDU3NSBdCi9Gb250RGVzY3JpcHRvciA3IDAgUgovVG9Vbmljb2RlIDggMCBSCj4+
|
||||
CmVuZG9iagoKMTAgMCBvYmoKPDwvRjEgOSAwIFIKPj4KZW5kb2JqCgoxMSAwIG9iago8PC9G
|
||||
b250IDEwIDAgUgovUHJvY1NldFsvUERGL1RleHRdCj4+CmVuZG9iagoKMSAwIG9iago8PC9U
|
||||
eXBlL1BhZ2UvUGFyZW50IDQgMCBSL1Jlc291cmNlcyAxMSAwIFIvTWVkaWFCb3hbMCAwIDU5
|
||||
NSA4NDJdL0dyb3VwPDwvUy9UcmFuc3BhcmVuY3kvQ1MvRGV2aWNlUkdCL0kgdHJ1ZT4+L0Nv
|
||||
bnRlbnRzIDIgMCBSPj4KZW5kb2JqCgo0IDAgb2JqCjw8L1R5cGUvUGFnZXMKL1Jlc291cmNl
|
||||
cyAxMSAwIFIKL01lZGlhQm94WyAwIDAgNTk1IDg0MiBdCi9LaWRzWyAxIDAgUiBdCi9Db3Vu
|
||||
dCAxPj4KZW5kb2JqCgoxMiAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgNCAwIFIKL09w
|
||||
ZW5BY3Rpb25bMSAwIFIgL1hZWiBudWxsIG51bGwgMF0KL0xhbmcoZW4tTlopCj4+CmVuZG9i
|
||||
agoKMTMgMCBvYmoKPDwvQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5MDA3NDAwNjUwMDcyPgov
|
||||
UHJvZHVjZXI8RkVGRjAwNEMwMDY5MDA2MjAwNzIwMDY1MDA0RjAwNjYwMDY2MDA2OTAwNjMw
|
||||
MDY1MDAyMDAwMzUwMDJFMDAzMT4KL0NyZWF0aW9uRGF0ZShEOjIwMTYwNjE2MTM0NDU4KzEy
|
||||
JzAwJyk+PgplbmRvYmoKCnhyZWYKMCAxNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDgz
|
||||
NTggMDAwMDAgbiAKMDAwMDAwMDAxOSAwMDAwMCBuIAowMDAwMDAwMjE5IDAwMDAwIG4gCjAw
|
||||
MDAwMDg1MDEgMDAwMDAgbiAKMDAwMDAwMDIzOSAwMDAwMCBuIAowMDAwMDA3NTMzIDAwMDAw
|
||||
IG4gCjAwMDAwMDc1NTQgMDAwMDAgbiAKMDAwMDAwNzc0NyAwMDAwMCBuIAowMDAwMDA4MDgx
|
||||
IDAwMDAwIG4gCjAwMDAwMDgyNzEgMDAwMDAgbiAKMDAwMDAwODMwMyAwMDAwMCBuIAowMDAw
|
||||
MDA4NjAwIDAwMDAwIG4gCjAwMDAwMDg2OTcgMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDE0
|
||||
L1Jvb3QgMTIgMCBSCi9JbmZvIDEzIDAgUgovSUQgWyA8Nzg2RkVDMTY2OUIxOURDMTJBNEU2
|
||||
ODQzN0YxQjIzRTE+Cjw3ODZGRUMxNjY5QjE5REMxMkE0RTY4NDM3RjFCMjNFMT4gXQovRG9j
|
||||
Q2hlY2tzdW0gLzkzRjFCMUZBQjVENzc2Q0JFNDc2MzA1QzdENUVCRUUxCj4+CnN0YXJ0eHJl
|
||||
Zgo4ODcyCiUlRU9GCg==
|
||||
|
||||
--------------ae0qIOkrNQLQHe1YyfTsUXrk--
|
116
storage/testdata/plain-text.eml
vendored
Normal file
116
storage/testdata/plain-text.eml
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
Delivered-To: recipient@example.com
|
||||
Received: by 2002:a0c:fe87:0:0:0:0:0 with SMTP id d7csp146390qvs;
|
||||
Tue, 26 Jul 2022 20:45:20 -0700 (PDT)
|
||||
X-Received: by 2002:a17:90a:1943:b0:1ef:8146:f32f with SMTP id 3-20020a17090a194300b001ef8146f32fmr2327371pjh.112.1658893508159;
|
||||
Tue, 26 Jul 2022 20:45:08 -0700 (PDT)
|
||||
ARC-Seal: i=1; a=rsa-sha256; t=1658893507; cv=none;
|
||||
d=google.com; s=arc-20160816;
|
||||
b=KrXcumoy4Oldq3Ny6ZLUfED4+/+4ndNbrM3uw1COEhqCVWWv7lLfFeNHTyxJQJLBK3
|
||||
tVgmPBX2XRmX+531CFRNquUDrqhsvc4kgIq0ExWPz99wG2vgsKWQ2x89AIfQ8sEYMwxY
|
||||
HOwErTH6XQuJ45YE+5Lt4pjMP+7NqnJ1NTRQyc7FB/c1Wt1JdTWscgaJGqUMnIFSbCPG
|
||||
xi0xpJnrIkh4giARIhabCRmVoo1g8BfzYrmy8uHtbIcDDuCJ8tN2lMLscwfw3u8hZWm6
|
||||
e1nAx4iDYyShdMZPPoUVoMHDf9P39DKwhdfb/xP/cQ6ulv7ECzVSp5DM8aLpfjw6SU9G
|
||||
JYJA==
|
||||
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
|
||||
h=content-disposition:mime-version:message-id:subject:to:from:date
|
||||
:dkim-signature;
|
||||
bh=8shE8duj4atyKhQhO1qlS4/NgHN4ubjWq86U+mmAH9M=;
|
||||
b=TGK9vlNQRpyHvcpQonLjrFuLubL2mo9vT15CPwtC6ltsrYccKUozKiyb+id79dPatM
|
||||
y2unMpJqJFB4rZnASRm20Ck9dFRulM8bowO4l9BWKAUti9+u7bmLYbOPQCgDmJRA88ij
|
||||
YTkSKE8TuFMZQMJTkyZZTwE3F/Vrv84fAekWzGlwFoV3D6r6t1D5EUYUoR4xCVZdpMo1
|
||||
Ic0bEqgmRXl44uEqyVNpIC0w86Hzz84zl2V+nca+gxfObMzbJheDkOwVKkNNmr0ja936
|
||||
QZK+aO9s9VQGtqmjWtWhc1OWO50Bc5vE/krLFvZM6+vbMBEuDE5rkfHdf5mSD9Ix4xWl
|
||||
6/Rg==
|
||||
ARC-Authentication-Results: i=1; mx.google.com;
|
||||
dkim=pass header.i=@gmail.com header.s=20210112 header.b=fpxRepVP;
|
||||
spf=pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=sender@example.com;
|
||||
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com
|
||||
Return-Path: <sender@example.com>
|
||||
Received: from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.85.220.41])
|
||||
by mx.google.com with SMTPS id t3-20020a17090a2f8300b001f25e258dfasor335081pjd.34.2022.07.26.20.45.07
|
||||
for <recipient@example.com>
|
||||
(Google Transport Security);
|
||||
Tue, 26 Jul 2022 20:45:07 -0700 (PDT)
|
||||
Received-SPF: pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) client-ip=209.85.220.41;
|
||||
Authentication-Results: mx.google.com;
|
||||
dkim=pass header.i=@gmail.com header.s=20210112 header.b=fpxRepVP;
|
||||
spf=pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=sender@example.com;
|
||||
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=gmail.com; s=20210112;
|
||||
h=date:from:to:subject:message-id:mime-version:content-disposition;
|
||||
bh=8shE8duj4atyKhQhO1qlS4/NgHN4ubjWq86U+mmAH9M=;
|
||||
b=fpxRepVPdRgZF9VI4rCzO4n1l9+OHrm254/c1PaNcNnC1+0Rr78o1ASLvDKoQY4INc
|
||||
gRN1kJIk+ozQumJSfQPEIe+rHbJxe+wzjbYhEfUwBUnFHZykqvYWl6Xmjwg61IhxwwWk
|
||||
b3Gp/ODHkdQrm5QqIFACEn1fQmqkk4XBlcKMYEU/NOswGDOFULfbrhDcBWmR/gp2kHmT
|
||||
DkqRA9UJ1Cc6GO9lG+McRi8uLNaTymuLwzBydVV0bZOQTLxHQcQBTfUFrp/fwjHc9V19
|
||||
l9uQcn5rOOsh3vR37NGpv8WPi7BORLRFGjMVD0DZ7CtJwTDHz4EVvdLijt6YbUV9ecp1
|
||||
df3Q==
|
||||
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=1e100.net; s=20210112;
|
||||
h=x-gm-message-state:date:from:to:subject:message-id:mime-version
|
||||
:content-disposition;
|
||||
bh=8shE8duj4atyKhQhO1qlS4/NgHN4ubjWq86U+mmAH9M=;
|
||||
b=Z8ndxERf1NU67swjZ7cSjkSTTaa2YzhtrRyJkg0vnRxi87af7ECZNT+Zaxuxmxmqvb
|
||||
5T3IN2ymjPu1Y52EqRdZQpnzS/E5OjHbA6AYSn5qneNXNDxqJwp5qVSXuyB265QOo/9M
|
||||
bGp4fqfi8Qe5pmgkzyTqyrigWFOzcl23sCGXqvnrD8+0e+/n1dqo2tYk4v2KpSoAUxF0
|
||||
SNwHocpTDBDxOMEulUkQpqNlyZsgqNGdRhZmUN+2tQnpCQULd4B7+pydyWBCp9o8J1W4
|
||||
0IqmhJiNT8pB8MVzyUsWNG+WX9GBh8PK6XndOjmp2WvYh0LcUKeEYQ6zBsIdDFNEkMD1
|
||||
dU9w==
|
||||
X-Gm-Message-State: AJIora+ZXWhiNwKn6ik6LuIUHc1hskP3Nneo2J0m0wSC9wwGXI1RPi1a
|
||||
Ml5Ex/pAryQwTi7MXqbUQkCIrEe5kU0=
|
||||
X-Google-Smtp-Source: AGRyM1v7CWOR6/X4d18Wv11XTnkfT25QfmsqBowwGsebQlPqhR1ogD3bo1sZRs/OSAHP7AjywIebfw==
|
||||
X-Received: by 2002:a17:90a:5e0b:b0:1f0:5565:ee6e with SMTP id w11-20020a17090a5e0b00b001f05565ee6emr2290528pjf.128.1658893506447;
|
||||
Tue, 26 Jul 2022 20:45:06 -0700 (PDT)
|
||||
Return-Path: <sender@example.com>
|
||||
Received: from localhost.localhost ([8.8.8.8])
|
||||
by smtp.gmail.com with ESMTPSA id s7-20020a170902ea0700b0016a3f9e4865sm12488166plg.148.2022.07.26.20.45.04
|
||||
for <recipient@example.com>
|
||||
(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
|
||||
Tue, 26 Jul 2022 20:45:06 -0700 (PDT)
|
||||
Date: Wed, 27 Jul 2022 15:44:41 +1200
|
||||
From: Sender Smith <sender@example.com>
|
||||
To: Recipient Ross <recipient@example.com>
|
||||
Subject: Plain text message
|
||||
Message-ID: <20220727034441.7za34h6ljuzfpmj6@localhost.localhost>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=us-ascii
|
||||
Content-Disposition: inline
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras non massa lacinia,
|
||||
fringilla ex vel, ornare nulla. Suspendisse dapibus commodo sapien, non
|
||||
hendrerit diam feugiat sit amet. Nulla lorem quam, laoreet vitae nisl volutpat,
|
||||
mollis bibendum felis. In eget ultricies justo. Donec vitae hendrerit tortor, at
|
||||
posuere libero. Fusce a gravida nibh. Nulla ac odio ex.
|
||||
|
||||
Aliquam sem turpis, cursus vitae condimentum at, scelerisque pulvinar lectus.
|
||||
Cras tempor nisl ut arcu interdum, et luctus arcu cursus. Maecenas mollis
|
||||
sagittis commodo. Mauris ac lorem nec ex interdum consequat. Morbi congue
|
||||
ultrices ullamcorper. Aenean ex tortor, dapibus quis dapibus iaculis, iaculis
|
||||
eget felis. Vestibulum purus ante, efficitur in turpis ac, tristique laoreet
|
||||
orci. Nulla facilisi. Praesent mollis orci posuere elementum laoreet.
|
||||
Pellentesque enim nibh, varius at ante id, consequat posuere ante.
|
||||
|
||||
Cras maximus venenatis nulla nec cursus. Morbi convallis, enim eget viverra
|
||||
vulputate, ipsum arcu tincidunt tortor, ut cursus dui enim commodo quam. Donec
|
||||
et vulputate quam. Vivamus non posuere erat. Nam commodo pellentesque
|
||||
condimentum. Vivamus condimentum eros at odio dictum feugiat. Ut imperdiet
|
||||
tempor luctus. Aenean varius libero ac faucibus dictum. Aliquam sed finibus
|
||||
massa. Morbi dolor lorem, feugiat quis neque et, suscipit posuere ex. Sed auctor
|
||||
et augue at finibus. Vestibulum interdum mi ac justo porta aliquam. Curabitur
|
||||
nec enim sit amet enim aliquet accumsan. Etiam accumsan tellus tortor, interdum
|
||||
sodales odio finibus eu. Integer eget ante eu nisi lobortis pulvinar et vel
|
||||
ipsum. Cras condimentum posuere vulputate.
|
||||
|
||||
Cras nulla felis, blandit vitae egestas quis, fringilla ut dolor. Phasellus est
|
||||
augue, feugiat eu risus quis, posuere ultrices libero. Phasellus non nunc eget
|
||||
justo sollicitudin tincidunt. Praesent pretium dui id felis bibendum sodales.
|
||||
Phasellus eget dictum libero, auctor tempor nibh. Suspendisse posuere libero
|
||||
venenatis elit imperdiet porttitor. In condimentum dictum luctus. Nullam in
|
||||
nulla vitae augue blandit posuere. Vestibulum consectetur ultricies tincidunt.
|
||||
Vivamus dolor quam, pharetra sed eros sed, hendrerit ultrices diam. Vestibulum
|
||||
vulputate tellus eget tellus lacinia, a pulvinar velit vulputate. Suspendisse
|
||||
mauris odio, scelerisque eget turpis sed, tincidunt ultrices magna. Nunc arcu
|
||||
arcu, commodo et porttitor quis, accumsan viverra purus. Fusce id libero iaculis
|
||||
lorem tristique commodo porttitor id ipsum. Vestibulum odio dui, tincidunt eget
|
||||
lectus vel, tristique lacinia libero. Aliquam dapibus ac felis vitae cursus.
|
90
storage/utils.go
Normal file
90
storage/utils.go
Normal file
@ -0,0 +1,90 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/k3a/html2text"
|
||||
"github.com/ostafen/clover"
|
||||
)
|
||||
|
||||
// Return a header field as a []*mail.Address, or "null" is not found/empty
|
||||
func addressToSlice(env *enmime.Envelope, key string) []*mail.Address {
|
||||
data, _ := env.AddressList(key)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Generate the search text based on some header fields (to, from, subject etc)
|
||||
// and either the stripped HTML body (if exists) or text body
|
||||
func createSearchText(env *enmime.Envelope) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(env.GetHeader("From") + " ")
|
||||
b.WriteString(env.GetHeader("Subject") + " ")
|
||||
b.WriteString(env.GetHeader("To") + " ")
|
||||
b.WriteString(env.GetHeader("Cc") + " ")
|
||||
b.WriteString(env.GetHeader("Bcc") + " ")
|
||||
h := strings.TrimSpace(html2text.HTML2Text(env.HTML))
|
||||
if h != "" {
|
||||
b.WriteString(h + " ")
|
||||
} else {
|
||||
b.WriteString(env.Text + " ")
|
||||
}
|
||||
// add attachment filenames
|
||||
for _, a := range env.Attachments {
|
||||
b.WriteString(a.FileName + " ")
|
||||
}
|
||||
|
||||
d := b.String()
|
||||
|
||||
// remove/replace new lines
|
||||
re := regexp.MustCompile(`(\r?\n|\t|>|<|"|:|\,|;)`)
|
||||
d = re.ReplaceAllString(d, " ")
|
||||
// remove duplicate whitespace and trim
|
||||
d = strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(d)), " "))
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Auto-prune runs every 5 minutes to automatically delete oldest messages
|
||||
// if total is greater than the threshold
|
||||
func pruneCron() {
|
||||
for {
|
||||
// time.Sleep(5 * 60 * time.Second)
|
||||
time.Sleep(60 * time.Second)
|
||||
mailboxes, err := db.ListCollections()
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, m := range mailboxes {
|
||||
total, _ := db.Count(clover.NewQuery(m))
|
||||
if total > config.MaxMessages {
|
||||
limit := total - config.MaxMessages
|
||||
if limit > 5000 {
|
||||
limit = 5000
|
||||
}
|
||||
start := time.Now()
|
||||
if err := db.Delete(clover.NewQuery(m).
|
||||
Sort(clover.SortOption{Field: "Created", Direction: 1}).
|
||||
Limit(limit)); err != nil {
|
||||
logger.Log().Warnf("Error pruning: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
logger.Log().Infof("Pruned %d messages from %s in %s", limit, m, elapsed)
|
||||
if !strings.HasSuffix(m, "_data") {
|
||||
websockets.Broadcast("prune", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
260
updater/targz.go
Normal file
260
updater/targz.go
Normal file
@ -0,0 +1,260 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// TarGZExtract extracts a archive from the file inputFilePath.
|
||||
// It tries to create the directory structure outputFilePath contains if it doesn't exist.
|
||||
// It returns potential errors to be checked or nil if everything works.
|
||||
func TarGZExtract(inputFilePath, outputFilePath string) (err error) {
|
||||
outputFilePath = stripTrailingSlashes(outputFilePath)
|
||||
inputFilePath, outputFilePath, err = makeAbsolute(inputFilePath, outputFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
undoDir, err := mkdirAll(outputFilePath, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
undoDir()
|
||||
}
|
||||
}()
|
||||
|
||||
return extract(inputFilePath, outputFilePath)
|
||||
}
|
||||
|
||||
// Creates all directories with os.MakedirAll and returns a function to remove the first created directory so cleanup is possible.
|
||||
func mkdirAll(dirPath string, perm os.FileMode) (func(), error) {
|
||||
var undoDir string
|
||||
|
||||
for p := dirPath; ; p = filepath.Dir(p) {
|
||||
finfo, err := os.Stat(p)
|
||||
if err == nil {
|
||||
if finfo.IsDir() {
|
||||
break
|
||||
}
|
||||
|
||||
finfo, err = os.Lstat(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if finfo.IsDir() {
|
||||
break
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("mkdirAll (%s): %v", p, syscall.ENOTDIR)
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
undoDir = p
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if undoDir == "" {
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dirPath, perm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func() {
|
||||
if err := os.RemoveAll(undoDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Remove trailing slash if any.
|
||||
func stripTrailingSlashes(path string) string {
|
||||
if len(path) > 0 && path[len(path)-1] == '/' {
|
||||
path = path[0 : len(path)-1]
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// Make input and output paths absolute.
|
||||
func makeAbsolute(inputFilePath, outputFilePath string) (string, string, error) {
|
||||
inputFilePath, err := filepath.Abs(inputFilePath)
|
||||
if err == nil {
|
||||
outputFilePath, err = filepath.Abs(outputFilePath)
|
||||
}
|
||||
|
||||
return inputFilePath, outputFilePath, err
|
||||
}
|
||||
|
||||
// Write path without the prefix in subPath to tar writer.
|
||||
func writeTarGz(path string, tarWriter *tar.Writer, fileInfo os.FileInfo, subPath string) error {
|
||||
file, err := os.Open(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
fmt.Printf("Error closing file: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
evaledPath, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subPath, err = filepath.EvalSymlinks(subPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
link := ""
|
||||
if evaledPath != path {
|
||||
link = evaledPath
|
||||
}
|
||||
|
||||
header, err := tar.FileInfoHeader(fileInfo, link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = evaledPath[len(subPath):]
|
||||
|
||||
err = tarWriter.WriteHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(tarWriter, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract the file in filePath to directory.
|
||||
func extract(filePath string, directory string) error {
|
||||
file, err := os.Open(filepath.Clean(filePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
fmt.Printf("Error closing file: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
gzipReader, err := gzip.NewReader(bufio.NewReader(file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzipReader)
|
||||
|
||||
// Post extraction directory permissions & timestamps
|
||||
type DirInfo struct {
|
||||
Path string
|
||||
Header *tar.Header
|
||||
}
|
||||
|
||||
// slice to add all extracted directory info for post-processing
|
||||
postExtraction := []DirInfo{}
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileInfo := header.FileInfo()
|
||||
dir := filepath.Join(directory, filepath.Dir(header.Name))
|
||||
filename := filepath.Join(dir, fileInfo.Name())
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
// create the directory 755 in case writing permissions prohibit writing before files added
|
||||
if err := os.MkdirAll(filename, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set file ownership (if allowed)
|
||||
// Chtimes() && Chmod() only set after once extraction is complete
|
||||
os.Chown(filename, header.Uid, header.Gid) // #nosec
|
||||
|
||||
// add directory info to slice to process afterwards
|
||||
postExtraction = append(postExtraction, DirInfo{filename, header})
|
||||
continue
|
||||
}
|
||||
|
||||
// make sure parent directory exists (may not be included in tar)
|
||||
if !fileInfo.IsDir() && !isDir(dir) {
|
||||
err = os.MkdirAll(dir, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Create(filepath.Clean(filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writer := bufio.NewWriter(file)
|
||||
|
||||
buffer := make([]byte, 4096)
|
||||
for {
|
||||
n, err := tarReader.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
_, err = writer.Write(buffer[:n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = writer.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set file permissions, timestamps & uid/gid
|
||||
os.Chmod(filename, os.FileMode(header.Mode)) // #nosec
|
||||
os.Chtimes(filename, header.AccessTime, header.ModTime) // #nosec
|
||||
os.Chown(filename, header.Uid, header.Gid) // #nosec
|
||||
}
|
||||
|
||||
if len(postExtraction) > 0 {
|
||||
for _, dir := range postExtraction {
|
||||
os.Chtimes(dir.Path, dir.Header.AccessTime, dir.Header.ModTime) // #nosec
|
||||
os.Chmod(dir.Path, dir.Header.FileInfo().Mode().Perm()) // #nosec
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
75
updater/unzip.go
Normal file
75
updater/unzip.go
Normal file
@ -0,0 +1,75 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Unzip will decompress a zip archive, moving all files and folders
|
||||
// within the zip file (src) to an output directory (dest).
|
||||
func Unzip(src string, dest string) ([]string, error) {
|
||||
|
||||
var filenames []string
|
||||
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
|
||||
// Store filename/path for returning and using later on
|
||||
fpath := filepath.Join(dest, filepath.Clean(f.Name))
|
||||
|
||||
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
|
||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
return filenames, fmt.Errorf("%s: illegal file path", fpath)
|
||||
}
|
||||
|
||||
filenames = append(filenames, fpath)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
// Make Folder
|
||||
if err := os.MkdirAll(fpath, os.ModePerm); err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Make File
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(filepath.Clean(fpath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, rc) // #nosec - file is streamed from zip to file
|
||||
|
||||
// Close the file without defer to close before next iteration of loop
|
||||
if err := outFile.Close(); err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
if err := rc.Close(); err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
}
|
||||
return filenames, nil
|
||||
}
|
344
updater/updater.go
Normal file
344
updater/updater.go
Normal file
@ -0,0 +1,344 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/semver"
|
||||
)
|
||||
|
||||
var (
|
||||
// AllowPrereleases defines whether pre-releases may be included
|
||||
AllowPrereleases = false
|
||||
|
||||
tempDir string
|
||||
)
|
||||
|
||||
// Releases struct for Github releases json
|
||||
type Releases []struct {
|
||||
Name string `json:"name"` // release name
|
||||
Tag string `json:"tag_name"` // release tag
|
||||
Prerelease bool `json:"prerelease"` // Github pre-release
|
||||
Assets []struct {
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
} `json:"assets"`
|
||||
}
|
||||
|
||||
// Release struct contains the file data for downloadable release
|
||||
type Release struct {
|
||||
Name string
|
||||
Tag string
|
||||
URL string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// GithubLatest fetches the latest release info & returns release tag, filename & download url
|
||||
func GithubLatest(repo, name string) (string, string, string, error) {
|
||||
releaseURL := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo)
|
||||
|
||||
resp, err := http.Get(releaseURL) // #nosec
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
linkOS := runtime.GOOS
|
||||
linkArch := runtime.GOARCH
|
||||
linkExt := ".tar.gz"
|
||||
if linkOS == "windows" {
|
||||
// Windows uses .zip instead
|
||||
linkExt = ".zip"
|
||||
}
|
||||
|
||||
var allReleases = []Release{}
|
||||
|
||||
var releases Releases
|
||||
|
||||
if err := json.Unmarshal(body, &releases); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
archiveName := fmt.Sprintf("%s_%s_%s%s", name, linkOS, linkArch, linkExt)
|
||||
|
||||
// loop through releases
|
||||
for _, r := range releases {
|
||||
if !semver.IsValid(r.Tag) {
|
||||
// Invalid semversion, skip
|
||||
continue
|
||||
}
|
||||
|
||||
if !AllowPrereleases && (semver.Prerelease(r.Tag) != "" || r.Prerelease) {
|
||||
// we don't accept AllowPrereleases, skip
|
||||
continue
|
||||
}
|
||||
|
||||
for _, a := range r.Assets {
|
||||
if a.Name == archiveName {
|
||||
thisRelease := Release{a.Name, r.Tag, a.BrowserDownloadURL, a.Size}
|
||||
allReleases = append(allReleases, thisRelease)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(allReleases) == 0 {
|
||||
// no releases with suitable assets found
|
||||
return "", "", "", fmt.Errorf("No binary releases found")
|
||||
}
|
||||
|
||||
var latestRelease = Release{}
|
||||
|
||||
for _, r := range allReleases {
|
||||
// detect the latest release
|
||||
if semver.Compare(r.Tag, latestRelease.Tag) == 1 {
|
||||
latestRelease = r
|
||||
}
|
||||
}
|
||||
|
||||
return latestRelease.Tag, latestRelease.Name, latestRelease.URL, nil
|
||||
}
|
||||
|
||||
// GreaterThan compares the current version to a different version
|
||||
// returning < 1 not upgradeable
|
||||
func GreaterThan(toVer, fromVer string) bool {
|
||||
return semver.Compare(toVer, fromVer) == 1
|
||||
}
|
||||
|
||||
// GithubUpdate the running binary with the latest release binary from Github
|
||||
func GithubUpdate(repo, appName, currentVersion string) (string, error) {
|
||||
ver, filename, downloadURL, err := GithubLatest(repo, appName)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ver == currentVersion {
|
||||
return "", fmt.Errorf("No new release found")
|
||||
}
|
||||
|
||||
if semver.Compare(ver, currentVersion) < 1 {
|
||||
return "", fmt.Errorf("No newer releases found (latest %s)", ver)
|
||||
}
|
||||
|
||||
tmpDir := getTempDir()
|
||||
|
||||
// outFile can be a tar.gz or a zip, depending on architecture
|
||||
outFile := filepath.Join(tmpDir, filename)
|
||||
|
||||
if err := downloadToFile(downloadURL, outFile); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newExec := filepath.Join(tmpDir, "golp")
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if _, err := Unzip(outFile, tmpDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
newExec = filepath.Join(tmpDir, "golp.exe")
|
||||
} else {
|
||||
if err := TarGZExtract(outFile, tmpDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
/* #nosec G302 */
|
||||
if err := os.Chmod(newExec, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// ensure the new binary is executable (mainly for inconsistent darwin builds)
|
||||
/* #nosec G204 */
|
||||
cmd := exec.Command(newExec)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get the running binary
|
||||
oldExec, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = replaceFile(oldExec, newExec); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
// DownloadToFile downloads a URL to a file
|
||||
func downloadToFile(url, fileName string) error {
|
||||
// Get the data
|
||||
resp, err := http.Get(url) // #nosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Create the file
|
||||
out, err := os.Create(filepath.Clean(fileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := out.Close(); err != nil {
|
||||
logger.Log().Errorf("Error closing file: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Write the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ReplaceFile replaces one file with another.
|
||||
// Running files cannot be overwritten, so it has to be moved
|
||||
// and the new binary saved to the original path. This requires
|
||||
// read & write permissions to both the original file and directory.
|
||||
// Note, on Windows it is not possible to delete a running program,
|
||||
// so the old exe is renamed and moved to os.TempDir()
|
||||
func replaceFile(dst, src string) error {
|
||||
// open the source file for reading
|
||||
source, err := os.Open(filepath.Clean(src))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// destination directory eg: /usr/local/bin
|
||||
dstDir := filepath.Dir(dst)
|
||||
// binary filename
|
||||
binaryFilename := filepath.Base(dst)
|
||||
// old binary tmp name
|
||||
dstOld := fmt.Sprintf("%s.old", binaryFilename)
|
||||
// new binary tmp name
|
||||
dstNew := fmt.Sprintf("%s.new", binaryFilename)
|
||||
// absolute path of new tmp file
|
||||
newTmpAbs := filepath.Join(dstDir, dstNew)
|
||||
// absolute path of old tmp file
|
||||
oldTmpAbs := filepath.Join(dstDir, dstOld)
|
||||
|
||||
// get src permissions
|
||||
fi, _ := os.Stat(dst)
|
||||
srcPerms := fi.Mode().Perm()
|
||||
|
||||
// create the new file
|
||||
tmpNew, err := os.OpenFile(filepath.Clean(newTmpAbs), os.O_CREATE|os.O_RDWR, srcPerms) // #nosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy new binary to <binary>.new
|
||||
if _, err := io.Copy(tmpNew, source); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// close immediately else Windows has a fit
|
||||
if err := tmpNew.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := source.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// rename the current executable to <binary>.old
|
||||
if err := os.Rename(dst, oldTmpAbs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// rename the <binary>.new to current executable
|
||||
if err := os.Rename(newTmpAbs, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete the old binary
|
||||
if runtime.GOOS == "windows" {
|
||||
tmpDir := os.TempDir()
|
||||
delFile := filepath.Join(tmpDir, filepath.Base(oldTmpAbs))
|
||||
if err := os.Rename(oldTmpAbs, delFile); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(oldTmpAbs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// remove the src file
|
||||
if err := os.Remove(src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTempDir will create & return a temporary directory if one has not been specified
|
||||
func getTempDir() string {
|
||||
if tempDir == "" {
|
||||
randBytes := make([]byte, 6)
|
||||
if _, err := rand.Read(randBytes); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tempDir = filepath.Join(os.TempDir(), "updater-"+hex.EncodeToString(randBytes))
|
||||
}
|
||||
if err := mkDirIfNotExists(tempDir); err != nil {
|
||||
// need a better way to exit
|
||||
logger.Log().Errorf("Error: %v", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
return tempDir
|
||||
}
|
||||
|
||||
// MkDirIfNotExists will create a directory if it doesn't exist
|
||||
func mkDirIfNotExists(path string) error {
|
||||
if !isDir(path) {
|
||||
return os.MkdirAll(path, os.ModePerm)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsFile returns if a path is a file
|
||||
func isFile(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if os.IsNotExist(err) || !info.Mode().IsRegular() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsDir returns if a path is a directory
|
||||
func isDir(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if os.IsNotExist(err) || !info.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user