Initial commit
This commit is contained in:
41
internal/command/clear_inbox.go
Normal file
41
internal/command/clear_inbox.go
Normal file
@ -0,0 +1,41 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/model"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type ClearInboxRequest struct{}
|
||||
|
||||
func HandleClearInbox(ctx context.Context, cmd cqrs.Command) error {
|
||||
_, ok := cmd.Request().(*ClearInboxRequest)
|
||||
if !ok {
|
||||
return cqrs.ErrUnexpectedRequest
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
db, err := storm.From(ctn)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve storm service")
|
||||
}
|
||||
|
||||
if err := db.Select().Delete(&model.Email{}); err != nil {
|
||||
if err == storm.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "could not delete emails")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
43
internal/command/delete_email.go
Normal file
43
internal/command/delete_email.go
Normal file
@ -0,0 +1,43 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/model"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type DeleteEmailRequest struct {
|
||||
EmailID int
|
||||
}
|
||||
|
||||
func HandleDeleteEmail(ctx context.Context, cmd cqrs.Command) error {
|
||||
req, ok := cmd.Request().(*DeleteEmailRequest)
|
||||
if !ok {
|
||||
return cqrs.ErrUnexpectedRequest
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
db, err := storm.From(ctn)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve storm service")
|
||||
}
|
||||
|
||||
if err := db.DeleteStruct(&model.Email{ID: req.EmailID}); err != nil {
|
||||
if err == storm.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "could not delete email")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
100
internal/command/store_email.go
Normal file
100
internal/command/store_email.go
Normal file
@ -0,0 +1,100 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/model"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type StoreEmailRequest struct {
|
||||
Envelope *enmime.Envelope
|
||||
}
|
||||
|
||||
func HandleStoreEmail(ctx context.Context, cmd cqrs.Command) error {
|
||||
req, ok := cmd.Request().(*StoreEmailRequest)
|
||||
if !ok {
|
||||
return cqrs.ErrUnexpectedRequest
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
db, err := storm.From(ctn)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve storm service")
|
||||
}
|
||||
|
||||
email := &model.Email{
|
||||
Headers: make(map[string][]string),
|
||||
SentAt: time.Now(),
|
||||
}
|
||||
|
||||
email.HTML = req.Envelope.HTML
|
||||
email.Text = req.Envelope.Text
|
||||
|
||||
for _, k := range req.Envelope.GetHeaderKeys() {
|
||||
switch k {
|
||||
case "From":
|
||||
from, err := req.Envelope.AddressList(k)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not retrieve '%s' adresses", k)
|
||||
}
|
||||
|
||||
email.From = from
|
||||
|
||||
case "To":
|
||||
to, err := req.Envelope.AddressList(k)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not retrieve '%s' adresses", k)
|
||||
}
|
||||
|
||||
email.To = to
|
||||
|
||||
case "Cc":
|
||||
cc, err := req.Envelope.AddressList(k)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not retrieve '%s' adresses", k)
|
||||
}
|
||||
|
||||
email.Cc = cc
|
||||
|
||||
case "Cci":
|
||||
cci, err := req.Envelope.AddressList(k)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not retrieve '%s' adresses", k)
|
||||
}
|
||||
|
||||
email.Cci = cci
|
||||
|
||||
case "Subject":
|
||||
email.Subject = req.Envelope.GetHeader(k)
|
||||
|
||||
default:
|
||||
email.Headers[k] = req.Envelope.GetHeaderValues(k)
|
||||
}
|
||||
}
|
||||
|
||||
email.Attachments = make([]*model.Attachment, 0, len(req.Envelope.Attachments))
|
||||
for _, a := range req.Envelope.Attachments {
|
||||
email.Attachments = append(email.Attachments, &model.Attachment{
|
||||
ContentType: a.ContentType,
|
||||
Name: a.FileName,
|
||||
Data: a.Content,
|
||||
})
|
||||
}
|
||||
|
||||
if err := db.Save(email); err != nil {
|
||||
return errors.Wrap(err, "could not save email")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
108
internal/config/config.go
Normal file
108
internal/config/config.go
Normal file
@ -0,0 +1,108 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type SMTPConfig struct {
|
||||
Address string `yaml:"address" env:"FAKESMTP_SMTP_ADDRESS"`
|
||||
Username string `yaml:"username" env:"FAKESMTP_SMTP_USERNAME"`
|
||||
Password string `yaml:"password" env:"FAKESMTP_SMTP_PASSWORD"`
|
||||
Domain string `yaml:"domain" env:"FAKESMTP_SMTP_DOMAIN"`
|
||||
ReadTimeout time.Duration `yaml:"readTimeout" env:"FAKESMTP_SMTP_READTIMEOUT"`
|
||||
WriteTimeout time.Duration `yaml:"writeTimeout" env:"FAKESMTP_SMTP_WRITETIMEOUT"`
|
||||
MaxMessageBytes int `yaml:"maxMessageBytes" env:"FAKESMTP_SMTP_MAXMESSAGEBYTES"`
|
||||
MaxRecipients int `yaml:"maxRecipients" env:"FAKESMTP_SMTP_MAXRECIPIENTS"`
|
||||
AllowInsecureAuth bool `yaml:"allowInsecureAuth" env:"FAKESMTP_SMTP_ALLOWINSECUREAUTH"`
|
||||
Debug bool `yaml:"debug" env:"FAKESMTP_SMTP_DEBUG"`
|
||||
}
|
||||
|
||||
type DataConfig struct {
|
||||
Path string `yaml:"path" env:"FAKESMTP_DATA_PATH"`
|
||||
}
|
||||
|
||||
// NewFromFile retrieves the configuration from the given file
|
||||
func NewFromFile(filepath string) (*Config, error) {
|
||||
config := NewDefault()
|
||||
|
||||
data, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read file '%s'", filepath)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return nil, errors.Wrapf(err, "could not unmarshal configuration")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func WithEnvironment(conf *Config) error {
|
||||
if err := env.Parse(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDumpDefault() *Config {
|
||||
config := NewDefault()
|
||||
return config
|
||||
}
|
||||
|
||||
func NewDefault() *Config {
|
||||
return &Config{
|
||||
HTTP: HTTPConfig{
|
||||
Address: ":8080",
|
||||
TemplateDir: "template",
|
||||
PublicDir: "public",
|
||||
},
|
||||
SMTP: SMTPConfig{
|
||||
Address: ":2525",
|
||||
Username: "",
|
||||
Password: "",
|
||||
Domain: "localhost",
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
MaxMessageBytes: 1024 * 1024,
|
||||
MaxRecipients: 50,
|
||||
AllowInsecureAuth: true,
|
||||
Debug: true,
|
||||
},
|
||||
Data: DataConfig{
|
||||
Path: "fakesmtp.db",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Dump(config *Config, w io.Writer) error {
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not dump config")
|
||||
}
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
9
internal/config/provider.go
Normal file
9
internal/config/provider.go
Normal file
@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
import "gitlab.com/wpetit/goweb/service"
|
||||
|
||||
func ServiceProvider(config *Config) service.Provider {
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
return config, nil
|
||||
}
|
||||
}
|
33
internal/config/service.go
Normal file
33
internal/config/service.go
Normal file
@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "config"
|
||||
|
||||
// From retrieves the config service in the given container
|
||||
func From(container *service.Container) (*Config, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*Config)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the config service in the given container or panic otherwise
|
||||
func Must(container *service.Container) *Config {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
27
internal/model/email.go
Normal file
27
internal/model/email.go
Normal file
@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
ID int `storm:"id,increment"`
|
||||
HTML string
|
||||
Text string
|
||||
Headers map[string][]string
|
||||
From []*mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Cci []*mail.Address
|
||||
Subject string
|
||||
SentAt time.Time
|
||||
Attachments []*Attachment
|
||||
Seen bool `storm:"index"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Name string
|
||||
ContentType string
|
||||
Data []byte
|
||||
}
|
63
internal/query/get_inbox.go
Normal file
63
internal/query/get_inbox.go
Normal file
@ -0,0 +1,63 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/model"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type GetInboxRequest struct {
|
||||
OrderBy string
|
||||
Limit int
|
||||
Skip int
|
||||
Reverse bool
|
||||
}
|
||||
|
||||
type InboxData struct {
|
||||
Emails []*model.Email
|
||||
}
|
||||
|
||||
func HandleGetInbox(ctx context.Context, qry cqrs.Query) (interface{}, error) {
|
||||
req, ok := qry.Request().(*GetInboxRequest)
|
||||
if !ok {
|
||||
return nil, cqrs.ErrUnexpectedRequest
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
db, err := storm.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve storm service")
|
||||
}
|
||||
|
||||
emails := make([]*model.Email, 0)
|
||||
|
||||
query := db.Select()
|
||||
|
||||
if req.OrderBy != "" {
|
||||
query = query.OrderBy(req.OrderBy)
|
||||
} else {
|
||||
query = query.OrderBy("SentAt").Reverse()
|
||||
}
|
||||
|
||||
if req.Reverse {
|
||||
query = query.Reverse()
|
||||
}
|
||||
|
||||
if err := query.Find(&emails); err != nil {
|
||||
if err == storm.ErrNotFound {
|
||||
return &InboxData{emails}, nil
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "could not retrieve emails")
|
||||
}
|
||||
|
||||
return &InboxData{emails}, nil
|
||||
}
|
44
internal/query/open_email.go
Normal file
44
internal/query/open_email.go
Normal file
@ -0,0 +1,44 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/model"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type OpenEmailRequest struct {
|
||||
EmailID int
|
||||
}
|
||||
|
||||
type OpenEmailData struct {
|
||||
Email *model.Email
|
||||
}
|
||||
|
||||
func HandleOpenEmail(ctx context.Context, qry cqrs.Query) (interface{}, error) {
|
||||
req, ok := qry.Request().(*OpenEmailRequest)
|
||||
if !ok {
|
||||
return nil, cqrs.ErrUnexpectedRequest
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
db, err := storm.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve storm service")
|
||||
}
|
||||
|
||||
email := &model.Email{}
|
||||
|
||||
if err := db.One("ID", req.EmailID, email); err != nil {
|
||||
return nil, errors.Wrap(err, "could not find email")
|
||||
}
|
||||
|
||||
return &OpenEmailData{email}, nil
|
||||
}
|
200
internal/route/email.go
Normal file
200
internal/route/email.go
Normal file
@ -0,0 +1,200 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/command"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/model"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/query"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service/template"
|
||||
)
|
||||
|
||||
func serveEmailPage(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"))
|
||||
}
|
||||
|
||||
ctn := container.Must(ctx)
|
||||
tmpl := template.Must(ctn)
|
||||
|
||||
data := extendTemplateData(w, r, template.Data{
|
||||
"Email": email,
|
||||
})
|
||||
|
||||
if err := tmpl.RenderPage(w, "email.html.tmpl", data); err != nil {
|
||||
panic(errors.Wrapf(err, "could not render '%s' page", r.URL.Path))
|
||||
}
|
||||
}
|
||||
|
||||
func serveEmailHTMLContent(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"))
|
||||
}
|
||||
|
||||
html := email.HTML
|
||||
|
||||
if html == "" {
|
||||
http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
policy := bluemonday.UGCPolicy()
|
||||
sanitizedHTML := policy.Sanitize(email.HTML)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(sanitizedHTML))
|
||||
}
|
||||
|
||||
func serveEmailAttachment(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"))
|
||||
}
|
||||
|
||||
attachmentIndex, err := getAttachmentIndex(r)
|
||||
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 attachment"))
|
||||
}
|
||||
|
||||
if attachmentIndex < 0 || attachmentIndex >= len(email.Attachments) {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
attachment := email.Attachments[attachmentIndex]
|
||||
|
||||
w.Header().Set("Content-Type", attachment.ContentType)
|
||||
|
||||
if _, err := w.Write(attachment.Data); err != nil {
|
||||
panic(errors.Wrap(err, "could not send attachment"))
|
||||
}
|
||||
}
|
||||
|
||||
func handleEmailDelete(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()
|
||||
ctn := container.Must(ctx)
|
||||
bus := cqrs.Must(ctn)
|
||||
|
||||
deleteEmail := &command.DeleteEmailRequest{
|
||||
EmailID: emailID,
|
||||
}
|
||||
|
||||
if _, err := bus.Exec(ctx, deleteEmail); err != nil {
|
||||
panic(errors.Wrap(err, "could not delete email"))
|
||||
}
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
|
||||
}
|
||||
|
||||
func getEmailID(r *http.Request) (int, error) {
|
||||
rawEmailID := chi.URLParam(r, "id")
|
||||
|
||||
emailID, err := strconv.ParseInt(rawEmailID, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(emailID), nil
|
||||
}
|
||||
|
||||
func openEmail(ctx context.Context, emailID int) (*model.Email, error) {
|
||||
ctn := container.Must(ctx)
|
||||
bus := cqrs.Must(ctn)
|
||||
req := &query.OpenEmailRequest{
|
||||
EmailID: int(emailID),
|
||||
}
|
||||
|
||||
result, err := bus.Query(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
openEmailData, ok := result.Data().(*query.OpenEmailData)
|
||||
if !ok {
|
||||
return nil, errors.New("unexpected result data")
|
||||
}
|
||||
|
||||
return openEmailData.Email, nil
|
||||
}
|
||||
|
||||
func getAttachmentIndex(r *http.Request) (int, error) {
|
||||
rawAttachmendIndex := chi.URLParam(r, "attachmendIndex")
|
||||
|
||||
attachmendIndex, err := strconv.ParseInt(rawAttachmendIndex, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(attachmendIndex), nil
|
||||
}
|
22
internal/route/helper.go
Normal file
22
internal/route/helper.go
Normal file
@ -0,0 +1,22 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service/template"
|
||||
)
|
||||
|
||||
func extendTemplateData(w http.ResponseWriter, r *http.Request, data template.Data) template.Data {
|
||||
ctn := container.Must(r.Context())
|
||||
data, err := template.Extend(data,
|
||||
template.WithBuildInfo(w, r, ctn),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not extend template data"))
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
55
internal/route/inbox.go
Normal file
55
internal/route/inbox.go
Normal file
@ -0,0 +1,55 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/command"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/query"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service/template"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
data := extendTemplateData(w, r, template.Data{
|
||||
"Emails": inboxData.Emails,
|
||||
})
|
||||
|
||||
if err := tmpl.RenderPage(w, "inbox.html.tmpl", data); err != nil {
|
||||
panic(errors.Wrapf(err, "could not render '%s' page", r.URL.Path))
|
||||
}
|
||||
}
|
||||
|
||||
func handleClearInbox(w http.ResponseWriter, r *http.Request) {
|
||||
ctn := container.Must(r.Context())
|
||||
|
||||
bus := cqrs.Must(ctn)
|
||||
|
||||
clearInbox := &command.ClearInboxRequest{}
|
||||
ctx := r.Context()
|
||||
|
||||
if _, err := bus.Exec(ctx, clearInbox); err != nil {
|
||||
panic(errors.Wrap(err, "could not clear inbox"))
|
||||
}
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
|
||||
}
|
24
internal/route/mount.go
Normal file
24
internal/route/mount.go
Normal file
@ -0,0 +1,24 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/config"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"gitlab.com/wpetit/goweb/static"
|
||||
)
|
||||
|
||||
func Mount(r *chi.Mux, config *config.Config) error {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Get("/", serveInboxPage)
|
||||
r.Delete("/emails", handleClearInbox)
|
||||
r.Get("/emails/{id}", serveEmailPage)
|
||||
r.Get("/emails/{id}/html", serveEmailHTMLContent)
|
||||
r.Get("/emails/{id}/attachments/{attachmendIndex}", serveEmailAttachment)
|
||||
r.Delete("/emails/{id}", handleEmailDelete)
|
||||
})
|
||||
|
||||
notFoundHandler := r.NotFoundHandler()
|
||||
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler))
|
||||
|
||||
return nil
|
||||
}
|
9
internal/storm/error.go
Normal file
9
internal/storm/error.go
Normal file
@ -0,0 +1,9 @@
|
||||
package storm
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = storm.ErrNotFound
|
||||
)
|
51
internal/storm/option.go
Normal file
51
internal/storm/option.go
Normal file
@ -0,0 +1,51 @@
|
||||
package storm
|
||||
|
||||
type Option struct {
|
||||
Path string
|
||||
Objects []interface{}
|
||||
ReIndex bool
|
||||
Init bool
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
|
||||
func DefaultOption() *Option {
|
||||
return MergeOption(
|
||||
&Option{},
|
||||
WithPath("data.db"),
|
||||
WithInit(true),
|
||||
WithReIndex(true),
|
||||
)
|
||||
}
|
||||
|
||||
func MergeOption(opt *Option, funcs ...OptionFunc) *Option {
|
||||
for _, fn := range funcs {
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
func WithPath(path string) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Path = path
|
||||
}
|
||||
}
|
||||
|
||||
func WithReIndex(reindex bool) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.ReIndex = reindex
|
||||
}
|
||||
}
|
||||
|
||||
func WithInit(init bool) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Init = init
|
||||
}
|
||||
}
|
||||
|
||||
func WithObjects(objects ...interface{}) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Objects = objects
|
||||
}
|
||||
}
|
48
internal/storm/provider.go
Normal file
48
internal/storm/provider.go
Normal file
@ -0,0 +1,48 @@
|
||||
package storm
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
func ServiceProvider(funcs ...OptionFunc) service.Provider {
|
||||
opt := MergeOption(
|
||||
DefaultOption(),
|
||||
funcs...,
|
||||
)
|
||||
|
||||
db, err := storm.Open(opt.Path)
|
||||
|
||||
if err == nil && opt.Objects != nil {
|
||||
err = migrate(db, opt.Objects, opt.Init, opt.ReIndex)
|
||||
}
|
||||
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
}
|
||||
|
||||
func migrate(db *storm.DB, objects []interface{}, init, reindex bool) error {
|
||||
for _, o := range objects {
|
||||
if init {
|
||||
if err := db.Init(o); err != nil {
|
||||
return errors.Wrapf(err, "could not init object '%s'", reflect.TypeOf(o).String())
|
||||
}
|
||||
}
|
||||
|
||||
if reindex {
|
||||
if err := db.ReIndex(o); err != nil {
|
||||
return errors.Wrapf(err, "could not reindex object '%s'", reflect.TypeOf(o).String())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
34
internal/storm/service.go
Normal file
34
internal/storm/service.go
Normal file
@ -0,0 +1,34 @@
|
||||
package storm
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "storm"
|
||||
|
||||
// From retrieves the storm service in the given container
|
||||
func From(container *service.Container) (*storm.DB, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*storm.DB)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the storm service in the given container or panic otherwise
|
||||
func Must(container *service.Container) *storm.DB {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
Reference in New Issue
Block a user