3 Commits

34 changed files with 285 additions and 869 deletions

View File

@ -5,5 +5,4 @@
/bin
/misc/docker/Dockerfile
/.env
/.env.dist
/tools
/.env.dist

3
.gitignore vendored
View File

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

1
.nvmrc
View File

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

View File

@ -1,15 +1,3 @@
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
@ -33,15 +21,13 @@ 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 tools
release: dist
@./misc/script/release.sh
dist:

View File

@ -55,18 +55,6 @@ 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
@ -91,14 +79,6 @@ 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

@ -53,11 +53,6 @@ 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,7 +12,6 @@ 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"
)
@ -75,31 +74,12 @@ 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 {
logger.Error(s.ctx, "could not exec command", logger.E(err))
return errors.Wrapf(err, "could not exec '%T' command", cmd)
return errors.Wrap(err, "could not exec command")
}
return nil

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="{{ .BaseURL }}/css/main.css" />
<link rel="stylesheet" href="/css/main.css" />
{{end}}
{{- block "head_script" . -}}
<script type="text/javascript" src="{{ .BaseURL }}/main.js"></script>
<script type="text/javascript" src="/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="{{ .BaseURL }}/" rel="Inbox" class="has-text-grey-dark">
<a href="/" 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="{{ .BaseURL }}/emails/{{ .Email.ID }}"
data-restful-endpoint="./{{ .Email.ID }}"
data-restful-method="DELETE"
data-restful-redirect="{{ .BaseURL }}/">
data-restful-redirect="../">
🗑️ Delete
</button>
{{end}}
@ -24,9 +24,8 @@
<h4 class="title is-size-4">Attachments ({{len .Email.Attachments}})</h4>
<ul>
{{ $email := .Email }}
{{ $baseURL := .BaseURL }}
{{range $i, $a := .Email.Attachments}}
<li><a href="{{ $baseURL }}/emails/{{ $email.ID }}/attachments/{{ $i }}" download="{{ $a.Name }}">{{ $a.Name }}</a></li>
<li><a href="{{ $email.ID }}/attachments/{{ $i }}" download="{{ $a.Name }}">{{ $a.Name }}</a></li>
{{end}}
</ul>
</div>
@ -45,7 +44,7 @@
data-controller="iframe"
data-action="load->iframe#onLoad"
style="width:100%;{{if not .Email.HTML}}display:none;{{end}}"
src="{{ .BaseURL }}/emails/{{ .Email.ID }}/html">
src="{{ .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="{{ .BaseURL }}/emails"
data-restful-endpoint="/emails"
data-restful-method="DELETE"
class="button is-danger">
🗑️ Clear
@ -24,11 +24,10 @@
</tr>
</thead>
<tbody>
{{ $baseURL := .BaseURL }}
{{range .Emails}}
<tr data-controller="inbox-entry"
data-action="click->inbox-entry#onClick"
data-inbox-entry-link="{{ $baseURL }}/emails/{{ .ID }}">
data-inbox-entry-link="./emails/{{ .ID }}">
<td class="email-subject"><div>{{ .Subject }}</div></td>
<td class="email-from">
{{range .From}}
@ -45,7 +44,7 @@
</td>
<td class="email-actions">
<div class="buttons is-right">
<a href="{{ $baseURL }}/emails/{{ .ID }}" class="button is-small is-link">👁️ See</a>
<a href="./emails/{{ .ID }}" class="button is-small is-link">👁️ See</a>
</div>
</td>
</tr>

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
9

14
debian/control vendored Normal file
View File

@ -0,0 +1,14 @@
Source: fake-smtp
Section: unknown
Priority: optional
Maintainer: William Petit <wpetit@cadoles.com>
Build-Depends: debhelper (>= 8.0.0), wget, ca-certificates, tar, curl
Standards-Version: 3.9.4
Homepage: http://forge.cadoles.com/wpetit/fake-smtp
Vcs-Git: http://forge.cadoles.com/wpetit/fake-smtp.git
Vcs-Browser: http://forge.cadoles.com/wpetit/fake-smtp
Package: fake-smtp
Architecture: amd64
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: Serveur SMTP factice pour le développement avec interface web

1
debian/fake-smtp.dirs vendored Normal file
View File

@ -0,0 +1 @@
var/lib/fake-smtp

11
debian/fake-smtp.service vendored Normal file
View File

@ -0,0 +1,11 @@
[Unit]
Description=Serveur SMTP factice pour le développement avec interface web
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/fake-smtp -workdir /usr/share/fake-smtp -config /etc/fake-smtp/config.yml
Restart=on-failure
[Install]
WantedBy=multi-user.target

54
debian/rules vendored Normal file
View File

@ -0,0 +1,54 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Uncomment this to turn on verbose mode.
export DH_VERBOSE=1
GO_VERSION := 1.14.2
OS := linux
ARCH := amd64
GOPATH=$(HOME)/go
ifeq (, $(shell which go 2>/dev/null))
override_dh_auto_build: install-go
endif
ifeq (, $(shell which node 2>/dev/null))
override_dh_auto_build: install-nodejs
endif
%:
dh $@ --with systemd
override_dh_auto_build: $(GOPATH)
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" make tooling
npm install
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" go mod vendor
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" ARCH_TARGETS=$(ARCH) make release
$(GOPATH):
mkdir -p $(GOPATH)
install-go:
wget https://dl.google.com/go/go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
tar -C /usr/local -xzf go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
install-nodejs:
curl -sL https://deb.nodesource.com/setup_13.x | bash -
apt-get install -y nodejs
override_dh_auto_install:
mkdir -p debian/fake-smtp/usr/share/fake-smtp
mkdir -p debian/fake-smtp/etc/fake-smtp
mkdir -p debian/fake-smtp/usr/bin
cp -r release/fake-smtp-$(OS)-$(ARCH)/* debian/fake-smtp/usr/share/fake-smtp/
mv debian/fake-smtp/usr/share/fake-smtp/bin/fake-smtp debian/fake-smtp/usr/bin/fake-smtp
mv debian/fake-smtp/usr/share/fake-smtp/config.yml debian/fake-smtp/etc/fake-smtp/config.yml
install -d debian/fake-smtp
override_dh_strip:
override_dh_auto_test:

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (native)

2
go.mod
View File

@ -5,7 +5,7 @@ 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-20200509203442-7bfe0ed36a21
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b // indirect
github.com/emersion/go-smtp v0.12.1
github.com/go-chi/chi v4.1.1+incompatible
github.com/jhillyerd/enmime v0.8.0

11
go.sum
View File

@ -51,9 +51,10 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
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-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=
@ -103,7 +104,9 @@ 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=
@ -156,6 +159,7 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
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=
@ -207,6 +211,7 @@ 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=
@ -230,6 +235,7 @@ 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=
@ -287,6 +293,7 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
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=

View File

@ -1,164 +0,0 @@
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,17 +12,15 @@ import (
)
type Config struct {
HTTP HTTPConfig `yaml:"http"`
SMTP SMTPConfig `yaml:"smtp"`
Data DataConfig `yaml:"data"`
Relay RelayConfig `yaml:"relay"`
HTTP HTTPConfig `yaml:"http"`
SMTP SMTPConfig `yaml:"smtp"`
Data DataConfig `ymal:"data"`
}
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 {
@ -38,18 +36,6 @@ 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"`
}
@ -105,9 +91,6 @@ func NewDefault() *Config {
Data: DataConfig{
Path: "fakesmtp.db",
},
Relay: RelayConfig{
Enabled: false,
},
}
}

View File

@ -1,195 +0,0 @@
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

@ -19,7 +19,6 @@ type InboxSearch struct {
From string
Body string
Subject string
Headers map[string]string
After time.Time
Before time.Time
}
@ -105,135 +104,55 @@ func HandleGetInbox(ctx context.Context, qry cqrs.Query) (interface{}, error) {
return &InboxData{emails}, nil
}
filtered := filterEmails(emails, req.Search)
filtered := make([]*model.Email, 0, len(emails))
return &InboxData{filtered}, nil
}
for _, eml := range emails {
match := true
var matchers = []emailMatcherFunc{
matchTo,
matchFrom,
matchBefore,
matchAfter,
matchHeaders,
}
if req.Search.To != "" {
found := false
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
for _, addr := range eml.To {
if strings.Contains(addr.Name, req.Search.To) || strings.Contains(addr.Address, req.Search.To) {
found = true
break
}
}
if !matchesHeader {
matches = false
break
if !found {
match = false
}
}
if !matches {
break
}
}
if req.Search.From != "" {
found := false
return matches
}
for _, addr := range eml.From {
if strings.Contains(addr.Name, req.Search.From) || strings.Contains(addr.Address, req.Search.From) {
found = true
func and(matchers ...emailMatcherFunc) emailMatcherFunc {
return func(eml *model.Email, search *InboxSearch) bool {
for _, match := range matchers {
if !match(eml, search) {
return false
break
}
}
if !found {
match = 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
if !req.Search.After.IsZero() && !eml.SentAt.After(req.Search.After) {
match = false
}
filtered = append(filtered, eml)
if !req.Search.Before.IsZero() && !eml.SentAt.Before(req.Search.Before) {
match = false
}
if match {
filtered = append(filtered, eml)
}
}
return filtered
return &InboxData{filtered}, nil
}

View File

@ -1,16 +1,13 @@
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"
)
@ -18,8 +15,8 @@ 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"))
}
@ -76,16 +73,6 @@ func createInboxQueryFromRequest(r *http.Request) (*query.GetInboxRequest, error
}
}
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
@ -107,10 +94,6 @@ func createInboxQueryFromRequest(r *http.Request) (*query.GetInboxRequest, error
search.Before = before
}
if rawHeaders != "" {
search.Headers = headers
}
inboxRequest := &query.GetInboxRequest{
OrderBy: orderBy,
Reverse: reverse == "y",
@ -121,16 +104,3 @@ func createInboxQueryFromRequest(r *http.Request) (*query.GetInboxRequest, error
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

@ -1,5 +0,0 @@
@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.19 AS build
FROM golang:1.13 AS build
ARG HTTP_PROXY=
ARG HTTPS_PROXY=
@ -7,15 +7,20 @@ ARG https_proxy=
RUN apt-get update && apt-get install -y build-essential git bash curl
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \
&& apt-get install -y nodejs
COPY . /src
WORKDIR /src
RUN cp -f misc/docker/config-patch.txt misc/release/config-patch.txt \
&& npm install \
RUN cp -f misc/docker/config-patch.yml misc/release/config-patch.yml
RUN go get github.com/krishicks/yaml-patch/cmd/yaml-patch
RUN npm install \
&& make vendor \
&& echo "---" > ./misc/release/config-patch.yml \
&& make ARCH_TARGETS=amd64 release
FROM busybox
@ -26,6 +31,4 @@ WORKDIR /app
RUN mkdir -p /app
EXPOSE 8080 2525
CMD ["bin/fake-smtp", "--config", "config.yml"]

View File

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

View File

@ -0,0 +1,4 @@
---
- op: replace
path: /smtp/debug
value: false

View File

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

View File

@ -0,0 +1,10 @@
---
- 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

@ -73,17 +73,13 @@ 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 > "$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
go run "$PROJECT_DIR/cmd/$command" -dump-config > "$tmp_conf"
cat "$tmp_conf" | yaml-patch -o misc/release/config-patch.yml > "$patched_conf"
copy "$command" $os $arch "$patched_conf" "config.yml"
rm -f "$patched_conf"
rm -f "$tmp_conf" "$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.

356
package-lock.json generated
View File

@ -1398,9 +1398,9 @@
"dev": true
},
"aws4": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==",
"dev": true
},
"babel-loader": {
@ -1884,48 +1884,14 @@
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"dev": true,
"requires": {
"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"
}
}
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wrap-ansi": "^2.0.0"
}
},
"clone-deep": {
@ -3540,9 +3506,9 @@
"dev": true
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
"dev": true
},
"get-stdin": {
@ -3652,13 +3618,13 @@
"dev": true
},
"globule": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz",
"integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz",
"integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==",
"dev": true,
"requires": {
"glob": "~7.1.1",
"lodash": "~4.17.10",
"lodash": "~4.17.12",
"minimatch": "~3.0.2"
}
},
@ -3675,27 +3641,13 @@
"dev": true
},
"har-validator": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"dev": true,
"requires": {
"ajv": "^6.12.3",
"ajv": "^6.5.5",
"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": {
@ -3798,9 +3750,9 @@
}
},
"hosted-git-info": {
"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==",
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"dev": true
},
"http-signature": {
@ -3921,6 +3873,12 @@
"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",
@ -4127,9 +4085,9 @@
"dev": true
},
"js-base64": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"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==",
"dev": true
},
"js-tokens": {
@ -4201,6 +4159,15 @@
"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",
@ -4432,18 +4399,18 @@
}
},
"mime-db": {
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
"integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==",
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
"dev": true
},
"mime-types": {
"version": "2.1.31",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
"integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"dev": true,
"requires": {
"mime-db": "1.48.0"
"mime-db": "1.43.0"
}
},
"mimic-fn": {
@ -4704,9 +4671,9 @@
"dev": true
},
"node-sass": {
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz",
"integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==",
"version": "4.13.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz",
"integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==",
"dev": true,
"requires": {
"async-foreach": "^0.1.3",
@ -4723,7 +4690,7 @@
"node-gyp": "^3.8.0",
"npmlog": "^4.0.0",
"request": "^2.88.0",
"sass-graph": "2.2.5",
"sass-graph": "^2.2.4",
"stdout-stream": "^1.4.0",
"true-case-path": "^1.0.2"
},
@ -4927,6 +4894,15 @@
"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",
@ -5659,9 +5635,9 @@
"dev": true
},
"require-main-filename": {
"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==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"dev": true
},
"resolve": {
@ -5875,15 +5851,15 @@
"dev": true
},
"sass-graph": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
"integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
"integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
"dev": true,
"requires": {
"glob": "^7.0.0",
"lodash": "^4.0.0",
"scss-tokenizer": "^0.2.3",
"yargs": "^13.3.2"
"yargs": "^7.0.0"
}
},
"sass-loader": {
@ -6206,9 +6182,9 @@
"dev": true
},
"spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
"integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
"integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
"dev": true,
"requires": {
"spdx-expression-parse": "^3.0.0",
@ -6216,15 +6192,15 @@
}
},
"spdx-exceptions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
"integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
"dev": true
},
"spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"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==",
"dev": true,
"requires": {
"spdx-exceptions": "^2.1.0",
@ -6232,9 +6208,9 @@
}
},
"spdx-license-ids": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz",
"integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==",
"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==",
"dev": true
},
"split-string": {
@ -7207,9 +7183,9 @@
}
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
"integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
"dev": true
},
"wide-align": {
@ -7231,48 +7207,13 @@
}
},
"wrap-ansi": {
"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==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {
"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"
}
}
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1"
}
},
"wrappy": {
@ -7288,9 +7229,9 @@
"dev": true
},
"y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true
},
"yallist": {
@ -7300,114 +7241,47 @@
"dev": true
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
"integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"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",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"require-main-filename": "^1.0.1",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
"string-width": "^1.0.2",
"which-module": "^1.0.0",
"y18n": "^3.2.1",
"yargs-parser": "^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
},
"find-up": {
"camelcase": {
"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=",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
"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": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
"integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
"camelcase": "^3.0.0"
},
"dependencies": {
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
"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.14.1",
"node-sass": "^4.10.0",
"resolve-url-loader": "^3.0.0",
"sass-loader": "^8.0.2",
"stimulus": "^1.1.0",