19 Commits

Author SHA1 Message Date
19e15d3fe7 Fix docker build recipe 2022-11-22 15:18:28 -06:00
db0a3ac98f Trigger Nuonet mirror sync 2022-03-16 16:58:15 +01:00
b335a825a3 Refactor inbox query filtering 2022-02-18 15:16:42 +01:00
f959fdb93f Update Dockerfile 2022-02-18 10:24:46 +01:00
6a52595fa7 Allow filtering emails via headers 2022-02-18 10:14:16 +01:00
450a9de82d Add missing base url in clear inbox endpoint url 2021-07-30 16:25:00 +02:00
3afd252c71 Upgrade node-sass 2021-07-26 14:48:24 +02:00
8027ba7bcc Add BaseURL config parameter 2021-07-26 14:47:58 +02:00
61c3cd33be Update README 2020-11-06 09:44:30 +01:00
62344993f5 Allow email relaying to a real MTA 2020-11-05 19:48:18 +01:00
9035280818 Tag Docker image with release date 2020-11-03 12:43:16 +01:00
82f21d0298 Upgrade to Go 1.15 2020-11-03 12:32:06 +01:00
92baa742c4 Add basic JSON API with filtering 2020-11-03 11:49:31 +01:00
14770d7146 Disable vendoring 2020-11-03 11:48:53 +01:00
c07753b21a Upgrade goweb dep 2020-11-03 11:48:38 +01:00
88660cdd78 Fix Docker packaging 2020-06-17 23:49:11 +02:00
f2530bf390 Fix anonymous login 2020-06-17 23:49:11 +02:00
66a06c9bf4 Mise à jour de 'README.md' 2020-06-11 15:22:46 +02:00
622f71d901 Mise à jour de 'README.md' 2020-06-11 14:59:52 +02:00
30 changed files with 1145 additions and 181 deletions

View File

@ -3,6 +3,7 @@
/vendor
/data
/bin
/misc/docker
/misc/docker/Dockerfile
/.env
/.env.dist
/.env.dist
/tools

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
/vendor
/bin
/node_modules
/.env
/.env
/tools

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v14.16.0

View File

@ -1,5 +1,17 @@
build: vendor
CGO_ENABLED=0 go build -mod=vendor -v -o bin/fake-smtp ./cmd/fake-smtp
DOCKER_DATE_TAG := $(shell date --utc +%Y.%-m.%-d%-H%-M)
YQ_VERSION ?= v4.30.4
YQ_BINARY ?= yq_linux_amd64
tools: tools/yq/bin/yq
tools/yq/bin/yq:
mkdir -p tools/yq/bin
wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY} -O tools/yq/bin/yq &&\
chmod +x tools/yq/bin/yq
build:
CGO_ENABLED=0 go build -v -o bin/fake-smtp ./cmd/fake-smtp
docker-image:
docker build \
@ -21,21 +33,20 @@ docker-run:
docker-release:
docker tag fake-smtp:latest bornholm/fake-smtp:latest
docker tag fake-smtp:latest bornholm/fake-smtp:$(DOCKER_DATE_TAG)
docker login
docker push bornholm/fake-smtp:latest
docker push bornholm/fake-smtp:$(DOCKER_DATE_TAG)
test:
go test -v -race ./...
release: dist
release: dist tools
@./misc/script/release.sh
dist:
npm run build
vendor:
go mod vendor
tidy:
go mod tidy

View File

@ -18,6 +18,8 @@ L'interface Web sera accessible à l'adresse http://localhost:8080/.
Le serveur SMTP sera accessible sur le port 2525. L'authentification est désactivée par défaut.
Voir la section "[Variables d'environnement](#variables-denvironnement)" pour voir comment personnaliser la configuration du service.
### Avec les binaires
> TODO
@ -53,6 +55,18 @@ smtp:
# Configuration du stockage
data:
path: fakesmtp.db
# Configuration du relais SMTP
relay:
enabled: false # Activer/désactiver le relais SMTP
address: "" # Adresse du serveur au format "host:port"
identity: "" # Identité du compte utilisateur, peut être laissé vide
username: "" # Identifiant du compte SMTP, non utilisé sur anonymous = true
password: "" # Mot de passe du compte SMTP, non utilisé sur anonymous = true
anonymous: false # Utiliser le mode d'authentification "anonyme"
useTLS: false # Utiliser TLS pour se connecter au serveur SMTP
insecureSkipVerify: true # Ne pas vérifier le certificat du serveur pour les connexions TLS/STARTTLS
fromOverride: "" # Surcharger l'adresse émetteur des courriels transmis
```
### Variables d'environnement
@ -77,6 +91,14 @@ Les valeurs des variables d'environnement surchargent les valeurs présentes dan
|`FAKESMTP_SMTP_ALLOWINSECUREAUTH`|`smtp.allowInsecureAuth`|
|`FAKESMTP_SMTP_DEBUG`|`smtp.debug`|
|`FAKESMTP_DATA_PATH`|`data.path`|
|`FAKESMTP_RELAY_ENABLED`|`relay.enabled`|
|`FAKESMTP_RELAY_ADDRESS`|`relay.address`|
|`FAKESMTP_RELAY_IDENTITY`|`relay.identity`|
|`FAKESMTP_RELAY_USERNAME`|`relay.username`|
|`FAKESMTP_RELAY_PASSWORD`|`relay.password`|
|`FAKESMTP_RELAY_ANONYMOUS`|`relay.anonymous`|
|`FAKESMTP_RELAY_INSECURE_SKIP_VERIFY`|`relay.insecureSkipVerify`|
|`FAKESMTP_RELAY_FROM_OVERRIDE`|`relay.fromOverride`|
## Démarrer avec les sources

View File

@ -21,7 +21,7 @@ func getServiceContainer(conf *config.Config) (*service.Container, error) {
// Create and expose template service provider
ctn.Provide(template.ServiceName, html.ServiceProvider(
conf.HTTP.TemplateDir,
html.NewDirectoryLoader(conf.HTTP.TemplateDir),
))
// Create and expose config service provider
@ -53,6 +53,11 @@ func getServiceContainer(conf *config.Config) (*service.Container, error) {
cqrs.CommandHandlerFunc(command.HandleDeleteEmail),
)
bus.RegisterCommand(
cqrs.MatchCommandRequest(&command.RelayEmailRequest{}),
cqrs.CommandHandlerFunc(command.HandleRelayEmail),
)
bus.RegisterQuery(
cqrs.MatchQueryRequest(&query.GetInboxRequest{}),
cqrs.QueryHandlerFunc(query.HandleGetInbox),

View File

@ -12,6 +12,7 @@ import (
"github.com/jhillyerd/enmime"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/logger"
"gitlab.com/wpetit/goweb/middleware/container"
"gitlab.com/wpetit/goweb/service"
)
@ -43,7 +44,9 @@ func (b *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, err
return nil, smtp.ErrAuthRequired
}
return nil, nil
ctx := context.WithValue(context.Background(), container.KeyServiceContainer, b.ctn)
return &Session{ctx, b.ctn, "anonymous"}, nil
}
// A Session is returned after successful login.
@ -72,12 +75,31 @@ func (s *Session) Data(r io.Reader) error {
return errors.Wrap(err, "could not retrieve cqrs service")
}
conf, err := config.From(s.ctn)
if err != nil {
return errors.Wrap(err, "could not retrieve config service")
}
if conf.Relay.Enabled {
cmd := &command.RelayEmailRequest{
Envelope: env,
}
if _, err := bus.Exec(s.ctx, cmd); err != nil {
logger.Error(s.ctx, "could not exec command", logger.E(err))
return errors.Wrapf(err, "could not exec '%T' command", cmd)
}
}
cmd := &command.StoreEmailRequest{
Envelope: env,
}
if _, err := bus.Exec(s.ctx, cmd); err != nil {
return errors.Wrap(err, "could not exec command")
logger.Error(s.ctx, "could not exec command", logger.E(err))
return errors.Wrapf(err, "could not exec '%T' command", cmd)
}
return nil
@ -105,6 +127,11 @@ func startSMTPServer(conf *config.Config, ctn *service.Container) {
s.MaxMessageBytes = conf.SMTP.MaxMessageBytes
s.MaxRecipients = conf.SMTP.MaxRecipients
s.AllowInsecureAuth = conf.SMTP.AllowInsecureAuth
if conf.SMTP.Username == "" && conf.SMTP.Password == "" {
s.AuthDisabled = true
}
if conf.SMTP.Debug {
s.Debug = os.Stdout
}

View File

@ -6,10 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" . -}}{{- end}}</title>
{{- block "head_style" . -}}
<link rel="stylesheet" href="/css/main.css" />
<link rel="stylesheet" href="{{ .BaseURL }}/css/main.css" />
{{end}}
{{- block "head_script" . -}}
<script type="text/javascript" src="/main.js"></script>
<script type="text/javascript" src="{{ .BaseURL }}/main.js"></script>
{{end}}
</head>
<body>

View File

@ -2,7 +2,7 @@
<div class="columns is-mobile">
<div class="column is-narrow">
<h1 class="is-size-3 title">
<a href="/" rel="Inbox" class="has-text-grey-dark">
<a href="{{ .BaseURL }}/" rel="Inbox" class="has-text-grey-dark">
{{if or .Emails .Email}}
📬
{{else}}

View File

@ -2,9 +2,9 @@
{{define "header_buttons"}}
<button class="button is-danger"
data-controller="restful"
data-restful-endpoint="./{{ .Email.ID }}"
data-restful-endpoint="{{ .BaseURL }}/emails/{{ .Email.ID }}"
data-restful-method="DELETE"
data-restful-redirect="../">
data-restful-redirect="{{ .BaseURL }}/">
🗑️ Delete
</button>
{{end}}
@ -24,8 +24,9 @@
<h4 class="title is-size-4">Attachments ({{len .Email.Attachments}})</h4>
<ul>
{{ $email := .Email }}
{{ $baseURL := .BaseURL }}
{{range $i, $a := .Email.Attachments}}
<li><a href="{{ $email.ID }}/attachments/{{ $i }}" download="{{ $a.Name }}">{{ $a.Name }}</a></li>
<li><a href="{{ $baseURL }}/emails/{{ $email.ID }}/attachments/{{ $i }}" download="{{ $a.Name }}">{{ $a.Name }}</a></li>
{{end}}
</ul>
</div>
@ -44,7 +45,7 @@
data-controller="iframe"
data-action="load->iframe#onLoad"
style="width:100%;{{if not .Email.HTML}}display:none;{{end}}"
src="{{ .Email.ID }}/html">
src="{{ .BaseURL }}/emails/{{ .Email.ID }}/html">
</iframe>
<div data-target="tabs.tabContent" data-tabs-for="text" style="{{if .Email.HTML}}display:none;{{end}}width:100%;overflow:hidden;">
<pre style="white-space:pre-line;">{{ .Email.Text }}</pre>

View File

@ -2,7 +2,7 @@
{{define "header_buttons"}}
<button
data-controller="restful"
data-restful-endpoint="/emails"
data-restful-endpoint="{{ .BaseURL }}/emails"
data-restful-method="DELETE"
class="button is-danger">
🗑️ Clear
@ -24,10 +24,11 @@
</tr>
</thead>
<tbody>
{{ $baseURL := .BaseURL }}
{{range .Emails}}
<tr data-controller="inbox-entry"
data-action="click->inbox-entry#onClick"
data-inbox-entry-link="./emails/{{ .ID }}">
data-inbox-entry-link="{{ $baseURL }}/emails/{{ .ID }}">
<td class="email-subject"><div>{{ .Subject }}</div></td>
<td class="email-from">
{{range .From}}
@ -44,7 +45,7 @@
</td>
<td class="email-actions">
<div class="buttons is-right">
<a href="./emails/{{ .ID }}" class="button is-small is-link">👁️ See</a>
<a href="{{ $baseURL }}/emails/{{ .ID }}" class="button is-small is-link">👁️ See</a>
</div>
</td>
</tr>

4
go.mod
View File

@ -5,13 +5,13 @@ go 1.14
require (
github.com/asdine/storm/v3 v3.1.1
github.com/caarlos0/env/v6 v6.2.1
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.12.1
github.com/go-chi/chi v4.1.1+incompatible
github.com/jhillyerd/enmime v0.8.0
github.com/microcosm-cc/bluemonday v1.0.2
github.com/pkg/errors v0.9.1
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
gopkg.in/yaml.v2 v2.2.8
)

40
go.sum
View File

@ -1,3 +1,4 @@
cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ=
cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@ -6,6 +7,7 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ=
cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
@ -21,12 +23,16 @@ github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtix
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw=
github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/asdine/storm/v3 v3.1.1 h1:5ESJvmcNhQQOFcvpxkIHcZs7mp8Z6XGdBqEoAgf+11g=
github.com/asdine/storm/v3 v3.1.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
@ -37,33 +43,39 @@ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
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/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q=
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.12.1 h1:1R8BDqrR2HhlGwgFYcOi+BVTvK1bMjAB65QcVpJ5sNA=
github.com/emersion/go-smtp v0.12.1/go.mod h1:SD9V/xa4ndMw77lR3Mf7htkp8RBNYuPh9UeuBs9tpUQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4=
github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -78,6 +90,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
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.2-0.20191216170541-340f1ebe299e h1:4WfjkTUTsO6siF8ghDQQk6t7x/FPsv3w6MXkc47do7Q=
github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -90,9 +103,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -109,11 +120,14 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
@ -135,13 +149,13 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@ -149,16 +163,18 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce h1:B3inZUHFr/FpA3jb+ZeSSHk3FSpB0xkQ0TjePhRokxw=
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce/go.mod h1:Gfv7cBOw1T2XwXMsLm1d9kAjMAdNtLMjPv+yCzRO9qk=
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2 h1:9WJw0v6BzHV8fP8EywjcqAz8PyCZxLn8ioTiMP4SBog=
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2/go.mod h1:Gfv7cBOw1T2XwXMsLm1d9kAjMAdNtLMjPv+yCzRO9qk=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -191,7 +207,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@ -215,7 +230,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -242,6 +256,7 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -263,19 +278,22 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
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.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
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/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=

View File

@ -0,0 +1,164 @@
package command
import (
"context"
"crypto/tls"
"io"
"sync"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"forge.cadoles.com/wpetit/fake-smtp/internal/config"
"github.com/jhillyerd/enmime"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/middleware/container"
)
type RelayEmailRequest struct {
Envelope *enmime.Envelope
}
func HandleRelayEmail(ctx context.Context, cmd cqrs.Command) error {
req, ok := cmd.Request().(*RelayEmailRequest)
if !ok {
return cqrs.ErrUnexpectedRequest
}
ctn, err := container.From(ctx)
if err != nil {
return errors.Wrap(err, "could not retrieve service container")
}
conf, err := config.From(ctn)
if err != nil {
return errors.Wrap(err, "could not retrieve config service")
}
relay := conf.Relay
if err := forwardMail(req.Envelope, relay); err != nil {
return errors.Wrap(err, "could not forward mail")
}
return nil
}
func forwardMail(env *enmime.Envelope, conf config.RelayConfig) error {
var tlsConfig *tls.Config
if conf.InsecureSkipVerify {
tlsConfig = &tls.Config{
// nolint: gosec
InsecureSkipVerify: true,
}
}
addr := conf.Address
var (
client *smtp.Client
err error
)
if conf.UseTLS {
client, err = smtp.DialTLS(addr, tlsConfig)
if err != nil {
return errors.WithStack(err)
}
} else {
client, err = smtp.Dial(addr)
if err != nil {
return errors.WithStack(err)
}
}
defer client.Close()
if err = client.Hello("localhost"); err != nil {
return errors.WithStack(err)
}
if ok, _ := client.Extension("STARTTLS"); ok {
if err = client.StartTLS(tlsConfig); err != nil {
return errors.WithStack(err)
}
}
if conf.Username != "" || conf.Password != "" {
if ok, _ := client.Extension("AUTH"); ok {
var auth sasl.Client
if conf.Anonymous {
auth = sasl.NewAnonymousClient("fakesmtp")
} else {
auth = sasl.NewPlainClient(conf.Identity, conf.Username, conf.Password)
}
if err := client.Auth(auth); err != nil {
return errors.WithStack(err)
}
}
}
var from string
if conf.FromOverride != "" {
from = conf.FromOverride
} else {
from = env.GetHeader("From")
}
if err = client.Mail(from, nil); err != nil {
return errors.WithStack(err)
}
to := env.GetHeaderValues("To")
for _, addr := range to {
if err = client.Rcpt(addr); err != nil {
return errors.WithStack(err)
}
}
w, err := client.Data()
if err != nil {
return errors.WithStack(err)
}
pr, pw := io.Pipe()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
defer pw.Close()
if err = env.Root.Encode(pw); err != nil {
err = errors.WithStack(err)
}
}()
_, err = io.Copy(w, pr)
if err != nil {
return err
}
wg.Wait()
if err != nil {
return errors.WithStack(err)
}
if err = w.Close(); err != nil {
return err
}
if err := client.Quit(); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@ -12,15 +12,17 @@ import (
)
type Config struct {
HTTP HTTPConfig `yaml:"http"`
SMTP SMTPConfig `yaml:"smtp"`
Data DataConfig `ymal:"data"`
HTTP HTTPConfig `yaml:"http"`
SMTP SMTPConfig `yaml:"smtp"`
Data DataConfig `yaml:"data"`
Relay RelayConfig `yaml:"relay"`
}
type HTTPConfig struct {
Address string `yaml:"address" env:"FAKESMTP_HTTP_ADDRESS"`
TemplateDir string `yaml:"templateDir" env:"FAKESMTP_HTTP_TEMPLATEDIR"`
PublicDir string `yaml:"publicDir" env:"FAKESMTP_HTTP_PUBLICDIR"`
BaseURL string `yaml:"baseUrl" env:"FAKESMTP_HTTP_BASEURL"`
}
type SMTPConfig struct {
@ -36,6 +38,18 @@ type SMTPConfig struct {
Debug bool `yaml:"debug" env:"FAKESMTP_SMTP_DEBUG"`
}
type RelayConfig struct {
Enabled bool `yaml:"enabled" env:"FAKESMTP_RELAY_ENABLED"`
Address string `yaml:"address" env:"FAKESMTP_RELAY_ADDRESS"`
Identity string `yaml:"identity" env:"FAKESMTP_RELAY_IDENTITY"`
Username string `yaml:"username" env:"FAKESMTP_RELAY_USERNAME"`
Password string `yaml:"password" env:"FAKESMTP_RELAY_PASSWORD"`
Anonymous bool `yaml:"anonymous" env:"FAKESMTP_RELAY_ANONYMOUS"`
UseTLS bool `yaml:"useTLS" env:"FAKESMTP_RELAY_USE_TLS"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify" env:"FAKESMTP_RELAY_INSECURE_SKIP_VERIFY"`
FromOverride string `yaml:"fromOverride" env:"FAKESMTP_RELAY_FROM_OVERRIDE"`
}
type DataConfig struct {
Path string `yaml:"path" env:"FAKESMTP_DATA_PATH"`
}
@ -91,6 +105,9 @@ func NewDefault() *Config {
Data: DataConfig{
Path: "fakesmtp.db",
},
Relay: RelayConfig{
Enabled: false,
},
}
}

View File

@ -0,0 +1,195 @@
package query
import (
"net/mail"
"testing"
"time"
"forge.cadoles.com/wpetit/fake-smtp/internal/model"
"github.com/pkg/errors"
)
type emailMatcherTestCase struct {
Name string
Search *InboxSearch
Email *model.Email
Matcher emailMatcherFunc
Expect bool
}
func TestEmailMatcher(t *testing.T) {
t.Parallel()
johnDoeAddr, err := mail.ParseAddress("john.doe@test.local")
if err != nil {
t.Fatal(errors.WithStack(err))
}
adaLovelaceAddr, err := mail.ParseAddress("ada.lovelace@test.local")
if err != nil {
t.Fatal(errors.WithStack(err))
}
now := time.Now()
testCases := []emailMatcherTestCase{
{
Name: "Simple matching header",
Email: &model.Email{
Headers: map[string][]string{
"X-Swift-To": {johnDoeAddr.Address},
},
},
Search: &InboxSearch{
Headers: map[string]string{
"X-Swift-To": johnDoeAddr.Address,
},
},
Matcher: matchHeaders,
Expect: true,
},
{
Name: "Multiple matching header",
Email: &model.Email{
Headers: map[string][]string{
"X-Swift-To": {johnDoeAddr.Address},
"Content-Type": {"multipart/alternative; boundary=\"_=_swift_1645181013_7b80ab8ab386ba4fcaff4f6f79593adb_=_\""},
},
},
Search: &InboxSearch{
Headers: map[string]string{
"X-Swift-To": johnDoeAddr.Address,
"Content-Type": "multipart/alternative",
},
},
Matcher: matchHeaders,
Expect: true,
},
{
Name: "Simple non matching header",
Email: &model.Email{
Headers: map[string][]string{
"X-Swift-To": {johnDoeAddr.Address},
},
},
Search: &InboxSearch{
Headers: map[string]string{
"X-Swift-To": adaLovelaceAddr.Address,
},
},
Matcher: matchHeaders,
Expect: false,
},
{
Name: "Multiple non matching headers",
Email: &model.Email{
Headers: map[string][]string{
"X-Swift-To": {johnDoeAddr.Address},
"Content-Type": {"foo/bar"},
},
},
Search: &InboxSearch{
Headers: map[string]string{
"X-Swift-To": johnDoeAddr.Address,
"Content-Type": "multipart/alternative",
},
},
Matcher: matchHeaders,
Expect: false,
},
{
Name: "Simple to",
Email: &model.Email{
To: []*mail.Address{johnDoeAddr},
},
Search: &InboxSearch{
To: johnDoeAddr.Address,
},
Matcher: matchTo,
Expect: true,
},
{
Name: "Simple from",
Email: &model.Email{
From: []*mail.Address{johnDoeAddr},
},
Search: &InboxSearch{
From: johnDoeAddr.Address,
},
Matcher: matchFrom,
Expect: true,
},
{
Name: "Simple after",
Email: &model.Email{
SentAt: now,
},
Search: &InboxSearch{
After: now.Add(-5 * time.Second),
},
Matcher: matchAfter,
Expect: true,
},
{
Name: "Simple before",
Email: &model.Email{
SentAt: now,
},
Search: &InboxSearch{
Before: now.Add(5 * time.Second),
},
Matcher: matchBefore,
Expect: true,
},
{
Name: "Matching composite",
Email: &model.Email{
SentAt: now,
Headers: map[string][]string{
"X-Swift-To": {johnDoeAddr.Address},
"Content-Type": {"multipart/alternative; boundary=\"_=_swift_1645181013_7b80ab8ab386ba4fcaff4f6f79593adb_=_\""},
},
},
Search: &InboxSearch{
Before: now.Add(5 * time.Second),
Headers: map[string]string{
"X-Swift-To": johnDoeAddr.Address,
"Content-Type": "multipart/alternative",
},
},
Matcher: and(matchBefore, matchHeaders),
Expect: true,
},
{
Name: "Non matching composite",
Email: &model.Email{
SentAt: now,
Headers: map[string][]string{
"X-Swift-To": {johnDoeAddr.Address},
"Content-Type": {"multipart/alternative; boundary=\"_=_swift_1645181013_7b80ab8ab386ba4fcaff4f6f79593adb_=_\""},
},
},
Search: &InboxSearch{
Before: now.Add(5 * time.Second),
Headers: map[string]string{
"X-Swift-To": adaLovelaceAddr.Address,
"Content-Type": "multipart/alternative",
},
},
Matcher: and(matchBefore, matchHeaders),
Expect: false,
},
}
for _, tc := range testCases {
func(tc emailMatcherTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
if e, g := tc.Expect, tc.Matcher(tc.Email, tc.Search); e != g {
t.Errorf("'%s': expected '%v', got '%v'", tc.Name, e, g)
}
})
}(tc)
}
}

View File

@ -2,19 +2,34 @@ package query
import (
"context"
"strings"
"time"
"forge.cadoles.com/wpetit/fake-smtp/internal/model"
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
stormdb "github.com/asdine/storm/v3"
"github.com/asdine/storm/v3/q"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/middleware/container"
)
type InboxSearch struct {
To string
From string
Body string
Subject string
Headers map[string]string
After time.Time
Before time.Time
}
type GetInboxRequest struct {
OrderBy string
Limit int
Skip int
Reverse bool
Search *InboxSearch
}
type InboxData struct {
@ -39,7 +54,26 @@ func HandleGetInbox(ctx context.Context, qry cqrs.Query) (interface{}, error) {
emails := make([]*model.Email, 0)
query := db.Select()
var query stormdb.Query
if req.Search != nil {
matchers := make([]q.Matcher, 0)
if req.Search.Body != "" {
matchers = append(matchers, q.Or(
q.Re("HTML", req.Search.Body),
q.Re("Text", req.Search.Body),
))
}
if req.Search.Subject != "" {
matchers = append(matchers, q.Re("Subject", req.Search.Subject))
}
query = db.Select(matchers...)
} else {
query = db.Select()
}
if req.OrderBy != "" {
query = query.OrderBy(req.OrderBy)
@ -51,6 +85,14 @@ func HandleGetInbox(ctx context.Context, qry cqrs.Query) (interface{}, error) {
query = query.Reverse()
}
if req.Limit != 0 {
query = query.Limit(req.Limit)
}
if req.Skip != 0 {
query = query.Limit(req.Skip)
}
if err := query.Find(&emails); err != nil {
if err == storm.ErrNotFound {
return &InboxData{emails}, nil
@ -59,5 +101,139 @@ func HandleGetInbox(ctx context.Context, qry cqrs.Query) (interface{}, error) {
return nil, errors.Wrap(err, "could not retrieve emails")
}
return &InboxData{emails}, nil
if req.Search == nil {
return &InboxData{emails}, nil
}
filtered := filterEmails(emails, req.Search)
return &InboxData{filtered}, nil
}
var matchers = []emailMatcherFunc{
matchTo,
matchFrom,
matchBefore,
matchAfter,
matchHeaders,
}
type emailMatcherFunc func(*model.Email, *InboxSearch) bool
func matchTo(eml *model.Email, search *InboxSearch) bool {
if search.To == "" {
return true
}
found := false
for _, addr := range eml.To {
if strings.Contains(addr.Name, search.To) || strings.Contains(addr.Address, search.To) {
found = true
break
}
}
return found
}
func matchFrom(eml *model.Email, search *InboxSearch) bool {
if search.From == "" {
return true
}
found := false
for _, addr := range eml.From {
if strings.Contains(addr.Name, search.From) || strings.Contains(addr.Address, search.From) {
found = true
break
}
}
return found
}
func matchAfter(eml *model.Email, search *InboxSearch) bool {
if search.After.IsZero() {
return true
}
return eml.SentAt.After(search.After)
}
func matchBefore(eml *model.Email, search *InboxSearch) bool {
if search.Before.IsZero() {
return true
}
return eml.SentAt.Before(search.Before)
}
func matchHeaders(eml *model.Email, search *InboxSearch) bool {
if eml.Headers == nil {
return true
}
matches := true
for searchKey, searchValue := range search.Headers {
for headerKey, headerValues := range eml.Headers {
if searchKey != headerKey {
continue
}
matchesHeader := true
for _, hv := range headerValues {
if !strings.Contains(hv, searchValue) {
matchesHeader = false
break
}
}
if !matchesHeader {
matches = false
break
}
}
if !matches {
break
}
}
return matches
}
func and(matchers ...emailMatcherFunc) emailMatcherFunc {
return func(eml *model.Email, search *InboxSearch) bool {
for _, match := range matchers {
if !match(eml, search) {
return false
}
}
return true
}
}
func filterEmails(emails []*model.Email, search *InboxSearch) []*model.Email {
filtered := make([]*model.Email, 0)
match := and(matchers...)
for _, eml := range emails {
if !match(eml, search) {
continue
}
filtered = append(filtered, eml)
}
return filtered
}

64
internal/route/api.go Normal file
View File

@ -0,0 +1,64 @@
package route
import (
"net/http"
"forge.cadoles.com/wpetit/fake-smtp/internal/query"
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/logger"
"gitlab.com/wpetit/goweb/middleware/container"
)
func browseAPIV1Emails(w http.ResponseWriter, r *http.Request) {
ctn := container.Must(r.Context())
bus := cqrs.Must(ctn)
ctx := r.Context()
getInbox, err := createInboxQueryFromRequest(r)
if err != nil {
logger.Error(ctx, "bad request", logger.E(err))
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
result, err := bus.Query(ctx, getInbox)
if err != nil {
panic(errors.Wrap(err, "could not retrieve inbox"))
}
inboxData, ok := result.Data().(*query.InboxData)
if !ok {
panic(errors.New("unexpected data"))
}
api.DataResponse(w, http.StatusOK, inboxData)
}
func serveAPIV1Email(w http.ResponseWriter, r *http.Request) {
emailID, err := getEmailID(r)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
ctx := r.Context()
email, err := openEmail(ctx, emailID)
if err != nil {
if errors.Is(err, storm.ErrNotFound) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
panic(errors.Wrap(err, "could not open email"))
}
api.DataResponse(w, http.StatusOK, email)
}

View File

@ -1,10 +1,16 @@
package route
import (
"encoding/json"
"net/http"
"strconv"
"time"
"forge.cadoles.com/wpetit/fake-smtp/internal/config"
"forge.cadoles.com/wpetit/fake-smtp/internal/query"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container"
"gitlab.com/wpetit/goweb/service"
"gitlab.com/wpetit/goweb/service/template"
)
@ -12,11 +18,119 @@ func extendTemplateData(w http.ResponseWriter, r *http.Request, data template.Da
ctn := container.Must(r.Context())
data, err := template.Extend(data,
template.WithBuildInfo(w, r, ctn),
withBaseURL(ctn),
)
if err != nil {
panic(errors.Wrap(err, "could not extend template data"))
}
return data
}
func createInboxQueryFromRequest(r *http.Request) (*query.GetInboxRequest, error) {
orderBy := r.URL.Query().Get("orderBy")
reverse := r.URL.Query().Get("reverse")
to := r.URL.Query().Get("to")
from := r.URL.Query().Get("from")
body := r.URL.Query().Get("body")
var err error
var limit int64 = 0
rawLimit := r.URL.Query().Get("limit")
if rawLimit != "" {
limit, err = strconv.ParseInt(rawLimit, 10, 32)
if err != nil {
return nil, errors.WithStack(err)
}
}
var skip int64 = 0
rawSkip := r.URL.Query().Get("skip")
if rawSkip != "" {
skip, err = strconv.ParseInt(rawSkip, 10, 32)
if err != nil {
return nil, errors.WithStack(err)
}
}
var after time.Time
rawAfter := r.URL.Query().Get("after")
if rawAfter != "" {
after, err = time.Parse(time.RFC3339, rawAfter)
if err != nil {
return nil, errors.WithStack(err)
}
}
var before time.Time
rawBefore := r.URL.Query().Get("before")
if rawBefore != "" {
before, err = time.Parse(time.RFC3339, rawBefore)
if err != nil {
return nil, errors.WithStack(err)
}
}
var headers map[string]string
rawHeaders := r.URL.Query().Get("headers")
if rawHeaders != "" {
headers = make(map[string]string)
if err := json.Unmarshal([]byte(rawHeaders), &headers); err != nil {
return nil, errors.WithStack(err)
}
}
search := &query.InboxSearch{}
if to != "" {
search.To = to
}
if from != "" {
search.From = from
}
if body != "" {
search.Body = body
}
if rawAfter != "" {
search.After = after
}
if rawBefore != "" {
search.Before = before
}
if rawHeaders != "" {
search.Headers = headers
}
inboxRequest := &query.GetInboxRequest{
OrderBy: orderBy,
Reverse: reverse == "y",
Skip: int(skip),
Limit: int(limit),
Search: search,
}
return inboxRequest, nil
}
func withBaseURL(ctn *service.Container) template.DataExtFunc {
return func(data template.Data) (template.Data, error) {
conf, err := config.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
data["BaseURL"] = conf.HTTP.BaseURL
return data, nil
}
}

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/wpetit/fake-smtp/internal/query"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/logger"
"gitlab.com/wpetit/goweb/middleware/container"
"gitlab.com/wpetit/goweb/service/template"
)
@ -14,12 +15,18 @@ import (
func serveInboxPage(w http.ResponseWriter, r *http.Request) {
ctn := container.Must(r.Context())
tmpl := template.Must(ctn)
bus := cqrs.Must(ctn)
getInbox := &query.GetInboxRequest{}
ctx := r.Context()
getInbox, err := createInboxQueryFromRequest(r)
if err != nil {
logger.Error(ctx, "bad request", logger.E(err))
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
result, err := bus.Query(ctx, getInbox)
if err != nil {
panic(errors.Wrap(err, "could not retrieve inbox"))

View File

@ -17,6 +17,13 @@ func Mount(r *chi.Mux, config *config.Config) error {
r.Delete("/emails/{id}", handleEmailDelete)
})
r.Route("/api", func(r chi.Router) {
r.Route("/v1", func(r chi.Router) {
r.Get("/emails", browseAPIV1Emails)
r.Get("/emails/{id}", serveAPIV1Email)
})
})
notFoundHandler := r.NotFoundHandler()
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler))

5
misc/api.http Normal file
View File

@ -0,0 +1,5 @@
@baseURL = http://localhost:8080
### Filter emails via headers
GET {{ baseURL }}/api/v1/emails?headers={"Mime-Version":"1.0"}

View File

@ -1,4 +1,4 @@
FROM golang:1.13 AS build
FROM golang:1.19 AS build
ARG HTTP_PROXY=
ARG HTTPS_PROXY=
@ -7,15 +7,15 @@ ARG https_proxy=
RUN apt-get update && apt-get install -y build-essential git bash curl
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get install -y nodejs
COPY . /src
WORKDIR /src
RUN npm install \
&& make vendor \
RUN cp -f misc/docker/config-patch.txt misc/release/config-patch.txt \
&& npm install \
&& make ARCH_TARGETS=amd64 release
FROM busybox
@ -24,4 +24,8 @@ COPY --from=build /src/release/fake-smtp-linux-amd64 /app
WORKDIR /app
CMD ["bin/fake-smtp", "--config", "fake-smtp.yml"]
RUN mkdir -p /app
EXPOSE 8080 2525
CMD ["bin/fake-smtp", "--config", "config.yml"]

View File

@ -0,0 +1 @@
.smtp.debug = false

View File

@ -0,0 +1,3 @@
.data.path = "/var/lib/fake-smtp/data.db"
.smtp.address = "127.0.0.1:2525"
.smtp.debug = false

View File

@ -1,10 +0,0 @@
---
- op: replace
path: /data/path
value: /var/lib/fake-smtp/data.db
- op: replace
path: /smtp/address
value: 127.0.0.1:2525
- op: replace
path: /smtp/debug
value: false

View File

@ -24,7 +24,6 @@ function build {
echo "building $dirname..."
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build \
-mod=vendor \
-ldflags="-s -w -X 'main.GitRef=$(current_commit_ref)' -X 'main.ProjectVersion=$(current_version)' -X 'main.BuildDate=$(current_date)'" \
-gcflags=-trimpath="${PWD}" \
-asmflags=-trimpath="${PWD}" \
@ -74,13 +73,17 @@ function dump_default_conf {
local command=$1
local os=$2
local arch=$3
local tmp_conf=$(mktemp)
local patched_conf=$(mktemp)
go run "$PROJECT_DIR/cmd/$command" -dump-config > "$tmp_conf"
cat "$tmp_conf" | yaml-patch -o misc/release/config-patch.yml > "$patched_conf"
go run "$PROJECT_DIR/cmd/$command" -dump-config > "$patched_conf"
while IFS= read -r yq_cmd; do
echo "patching configuration with '$yq_cmd'..."
tools/yq/bin/yq -i "$yq_cmd" "$patched_conf"
done < misc/release/config-patch.txt
copy "$command" $os $arch "$patched_conf" "config.yml"
rm -f "$tmp_conf" "$patched_conf"
rm -f "$patched_conf"
}
function compress {

View File

@ -6,7 +6,7 @@ for i in {1..10}; do
-s "Test ${i}" \
-a README.md \
-a package.json \
foo_${i}@bar_${i}.com \
foo_${i}@bar${i}.com \
<<EOF
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus eget metus ornare, placerat mi nec, ornare magna. Vivamus volutpat nisi et aliquet mollis. Phasellus eu malesuada erat, vel molestie ipsum. Etiam tincidunt ligula eget scelerisque blandit. Vivamus nec quam vitae felis rutrum cursus a eget elit. Integer vestibulum ultrices iaculis. Integer varius sapien ac ante accumsan euismod. Quisque fermentum dui nec porttitor pellentesque. Vivamus lobortis mauris eget metus sagittis tempor.

358
package-lock.json generated
View File

@ -1398,9 +1398,9 @@
"dev": true
},
"aws4": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"dev": true
},
"babel-loader": {
@ -1884,14 +1884,48 @@
}
},
"cliui": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wrap-ansi": "^2.0.0"
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
}
}
},
"clone-deep": {
@ -3506,9 +3540,9 @@
"dev": true
},
"get-caller-file": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"get-stdin": {
@ -3618,13 +3652,13 @@
"dev": true
},
"globule": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz",
"integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz",
"integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==",
"dev": true,
"requires": {
"glob": "~7.1.1",
"lodash": "~4.17.12",
"lodash": "~4.17.10",
"minimatch": "~3.0.2"
}
},
@ -3641,13 +3675,27 @@
"dev": true
},
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"dev": true,
"requires": {
"ajv": "^6.5.5",
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
}
}
},
"has-ansi": {
@ -3750,9 +3798,9 @@
}
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"http-signature": {
@ -3873,12 +3921,6 @@
"loose-envify": "^1.0.0"
}
},
"invert-kv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
"dev": true
},
"is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
@ -4085,9 +4127,9 @@
"dev": true
},
"js-base64": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz",
"integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"dev": true
},
"js-tokens": {
@ -4159,15 +4201,6 @@
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true
},
"lcid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
"integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
"dev": true,
"requires": {
"invert-kv": "^1.0.0"
}
},
"leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -4399,18 +4432,18 @@
}
},
"mime-db": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
"integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==",
"dev": true
},
"mime-types": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"version": "2.1.31",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
"integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
"dev": true,
"requires": {
"mime-db": "1.43.0"
"mime-db": "1.48.0"
}
},
"mimic-fn": {
@ -4671,9 +4704,9 @@
"dev": true
},
"node-sass": {
"version": "4.13.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz",
"integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==",
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz",
"integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==",
"dev": true,
"requires": {
"async-foreach": "^0.1.3",
@ -4690,7 +4723,7 @@
"node-gyp": "^3.8.0",
"npmlog": "^4.0.0",
"request": "^2.88.0",
"sass-graph": "^2.2.4",
"sass-graph": "2.2.5",
"stdout-stream": "^1.4.0",
"true-case-path": "^1.0.2"
},
@ -4894,15 +4927,6 @@
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true
},
"os-locale": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
"dev": true,
"requires": {
"lcid": "^1.0.0"
}
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@ -5635,9 +5659,9 @@
"dev": true
},
"require-main-filename": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"resolve": {
@ -5851,15 +5875,15 @@
"dev": true
},
"sass-graph": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
"integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
"integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==",
"dev": true,
"requires": {
"glob": "^7.0.0",
"lodash": "^4.0.0",
"scss-tokenizer": "^0.2.3",
"yargs": "^7.0.0"
"yargs": "^13.3.2"
}
},
"sass-loader": {
@ -6182,9 +6206,9 @@
"dev": true
},
"spdx-correct": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
"integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
"integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
"dev": true,
"requires": {
"spdx-expression-parse": "^3.0.0",
@ -6192,15 +6216,15 @@
}
},
"spdx-exceptions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
"integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
"dev": true
},
"spdx-expression-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
"requires": {
"spdx-exceptions": "^2.1.0",
@ -6208,9 +6232,9 @@
}
},
"spdx-license-ids": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz",
"integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==",
"dev": true
},
"split-string": {
@ -7183,9 +7207,9 @@
}
},
"which-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
"integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
"wide-align": {
@ -7207,13 +7231,48 @@
}
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1"
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
}
}
},
"wrappy": {
@ -7229,9 +7288,9 @@
"dev": true
},
"y18n": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true
},
"yallist": {
@ -7241,47 +7300,114 @@
"dev": true
},
"yargs": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
"integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"camelcase": "^3.0.0",
"cliui": "^3.2.0",
"decamelize": "^1.1.1",
"get-caller-file": "^1.0.1",
"os-locale": "^1.4.0",
"read-pkg-up": "^1.0.1",
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^1.0.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^1.0.2",
"which-module": "^1.0.0",
"y18n": "^3.2.1",
"yargs-parser": "^5.0.0"
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
},
"dependencies": {
"camelcase": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
}
}
},
"yargs-parser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
"integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^3.0.0"
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"dependencies": {
"camelcase": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
}
}

View File

@ -20,7 +20,7 @@
"css-loader": "^3.5.2",
"file-loader": "^6.0.0",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.10.0",
"node-sass": "^4.14.1",
"resolve-url-loader": "^3.0.0",
"sass-loader": "^8.0.2",
"stimulus": "^1.1.0",