Initial commit
This commit is contained in:
67
cmd/fake-smtp/container.go
Normal file
67
cmd/fake-smtp/container.go
Normal file
@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gitlab.com/wpetit/goweb/template/html"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/command"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/config"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/query"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/storm"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
"gitlab.com/wpetit/goweb/service/build"
|
||||
"gitlab.com/wpetit/goweb/service/template"
|
||||
)
|
||||
|
||||
func getServiceContainer(conf *config.Config) (*service.Container, error) {
|
||||
// Initialize and configure service container
|
||||
ctn := service.NewContainer()
|
||||
|
||||
ctn.Provide(build.ServiceName, build.ServiceProvider(ProjectVersion, GitRef, BuildDate))
|
||||
|
||||
// Create and expose template service provider
|
||||
ctn.Provide(template.ServiceName, html.ServiceProvider(
|
||||
conf.HTTP.TemplateDir,
|
||||
))
|
||||
|
||||
// Create and expose config service provider
|
||||
ctn.Provide(config.ServiceName, config.ServiceProvider(conf))
|
||||
|
||||
ctn.Provide(storm.ServiceName, storm.ServiceProvider(
|
||||
storm.WithPath(conf.Data.Path),
|
||||
))
|
||||
|
||||
ctn.Provide(cqrs.ServiceName, cqrs.ServiceProvider())
|
||||
|
||||
bus, err := cqrs.From(ctn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bus.RegisterCommand(
|
||||
cqrs.MatchCommandRequest(&command.StoreEmailRequest{}),
|
||||
cqrs.CommandHandlerFunc(command.HandleStoreEmail),
|
||||
)
|
||||
|
||||
bus.RegisterCommand(
|
||||
cqrs.MatchCommandRequest(&command.ClearInboxRequest{}),
|
||||
cqrs.CommandHandlerFunc(command.HandleClearInbox),
|
||||
)
|
||||
|
||||
bus.RegisterCommand(
|
||||
cqrs.MatchCommandRequest(&command.DeleteEmailRequest{}),
|
||||
cqrs.CommandHandlerFunc(command.HandleDeleteEmail),
|
||||
)
|
||||
|
||||
bus.RegisterQuery(
|
||||
cqrs.MatchQueryRequest(&query.GetInboxRequest{}),
|
||||
cqrs.QueryHandlerFunc(query.HandleGetInbox),
|
||||
)
|
||||
|
||||
bus.RegisterQuery(
|
||||
cqrs.MatchQueryRequest(&query.OpenEmailRequest{}),
|
||||
cqrs.QueryHandlerFunc(query.HandleOpenEmail),
|
||||
)
|
||||
|
||||
return ctn, nil
|
||||
}
|
119
cmd/fake-smtp/main.go
Normal file
119
cmd/fake-smtp/main.go
Normal file
@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/route"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/config"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var (
|
||||
configFile = ""
|
||||
workdir = ""
|
||||
dumpConfig = false
|
||||
version = false
|
||||
)
|
||||
|
||||
// nolint: gochecknoglobals
|
||||
var (
|
||||
GitRef = "unknown"
|
||||
ProjectVersion = "unknown"
|
||||
BuildDate = "unknown"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
flag.StringVar(&configFile, "config", configFile, "configuration file")
|
||||
flag.StringVar(&workdir, "workdir", workdir, "working directory")
|
||||
flag.BoolVar(&dumpConfig, "dump-config", dumpConfig, "dump configuration and exit")
|
||||
flag.BoolVar(&version, "version", version, "show version and exit")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if version {
|
||||
fmt.Printf("%s (%s) - %s\n", ProjectVersion, GitRef, BuildDate)
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Switch to new working directory if defined
|
||||
if workdir != "" {
|
||||
if err := os.Chdir(workdir); err != nil {
|
||||
log.Fatalf("%+v", errors.Wrapf(err, "could not change working directory to '%s'", workdir))
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration file if defined, use default configuration otherwise
|
||||
var conf *config.Config
|
||||
|
||||
var err error
|
||||
|
||||
if configFile != "" {
|
||||
conf, err = config.NewFromFile(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.Wrapf(err, "could not load config file '%s'", configFile))
|
||||
}
|
||||
} else {
|
||||
if dumpConfig {
|
||||
conf = config.NewDumpDefault()
|
||||
} else {
|
||||
conf = config.NewDefault()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Dump configuration if asked
|
||||
if dumpConfig {
|
||||
if err := config.Dump(conf, os.Stdout); err != nil {
|
||||
log.Fatalf("%+v", errors.Wrap(err, "could not dump config"))
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if err := config.WithEnvironment(conf); err != nil {
|
||||
log.Fatalf("%+v", errors.Wrap(err, "could not override config with environment"))
|
||||
}
|
||||
|
||||
// Create service container
|
||||
ctn, err := getServiceContainer(conf)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.Wrap(err, "could not create service container"))
|
||||
}
|
||||
|
||||
go startSMTPServer(conf, ctn)
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Define base middlewares
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
// Expose service container on router
|
||||
r.Use(container.ServiceContainer(ctn))
|
||||
|
||||
// Define routes
|
||||
if err := route.Mount(r, conf); err != nil {
|
||||
log.Fatalf("%+v", errors.Wrap(err, "could not mount http routes"))
|
||||
}
|
||||
|
||||
log.Printf("listening on '%s'", conf.HTTP.Address)
|
||||
|
||||
if err := http.ListenAndServe(conf.HTTP.Address, r); err != nil {
|
||||
log.Fatalf("%+v", errors.Wrapf(err, "could not listen on '%s'", conf.HTTP.Address))
|
||||
}
|
||||
}
|
3
cmd/fake-smtp/public/dist/.gitignore
vendored
Normal file
3
cmd/fake-smtp/public/dist/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.gitkeep
|
0
cmd/fake-smtp/public/dist/.gitkeep
vendored
Normal file
0
cmd/fake-smtp/public/dist/.gitkeep
vendored
Normal file
11
cmd/fake-smtp/public/src/controllers/iframe_controller.js
Normal file
11
cmd/fake-smtp/public/src/controllers/iframe_controller.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { Controller } from "stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
onLoad(evt) {
|
||||
const iframe = evt.currentTarget;
|
||||
iframe.style.height = iframe.contentWindow.document.body.scrollHeight + 'px';
|
||||
|
||||
const links = iframe.contentWindow.document.querySelectorAll('a');
|
||||
Array.from(links).forEach(link => link.target = '_blank');
|
||||
}
|
||||
}
|
42
cmd/fake-smtp/public/src/controllers/restful_controller.js
Normal file
42
cmd/fake-smtp/public/src/controllers/restful_controller.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { Controller } from "stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.element.addEventListener('click', this.onClick);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.element.removeEventListener('click', this.onClick);
|
||||
}
|
||||
|
||||
onClick(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
const config = {
|
||||
method: this.data.get('method') || 'GET',
|
||||
};
|
||||
|
||||
if (this.data.has('payload')) {
|
||||
config.body = this.data.get('payload');
|
||||
}
|
||||
|
||||
const endpoint = this.data.get('endpoint');
|
||||
|
||||
fetch(endpoint, config)
|
||||
.then(res => {
|
||||
if (res.status < 200 && res.status >= 400) {
|
||||
throw new Error(`Unexpected server response: ${res.status} - ${res.statusText}`);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then(() => {
|
||||
const redirect = this.data.get('redirect');
|
||||
if (redirect) {
|
||||
window.location = redirect;
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
22
cmd/fake-smtp/public/src/controllers/tabs_controller.js
Normal file
22
cmd/fake-smtp/public/src/controllers/tabs_controller.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { Controller } from "stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = ["tab", "tabContent"]
|
||||
|
||||
openTab(evt) {
|
||||
const tab = evt.currentTarget;
|
||||
const tabName = tab.dataset.tabsName;
|
||||
|
||||
this.tabTargets.forEach(el => el.classList.remove('is-active'));
|
||||
evt.currentTarget.classList.add('is-active');
|
||||
|
||||
this.tabContentTargets.forEach(el => {
|
||||
if (el.dataset.tabsFor === tabName) {
|
||||
el.style.display = 'inherit';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
9
cmd/fake-smtp/public/src/index.js
Normal file
9
cmd/fake-smtp/public/src/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { Application } from "stimulus";
|
||||
import { definitionsFromContext } from "stimulus/webpack-helpers";
|
||||
|
||||
import "bulma/bulma.sass";
|
||||
import "./scss/main.scss";
|
||||
|
||||
const application = Application.start();
|
||||
const context = require.context("./controllers", true, /\.js$/);
|
||||
application.load(definitionsFromContext(context));
|
35
cmd/fake-smtp/public/src/scss/main.scss
Normal file
35
cmd/fake-smtp/public/src/scss/main.scss
Normal file
@ -0,0 +1,35 @@
|
||||
.inbox {
|
||||
table-layout: fixed;
|
||||
|
||||
td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
td > * {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.email-subject {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.email-from {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.email-to {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.email-sentat {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.email-actions {
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
}
|
116
cmd/fake-smtp/smtp.go
Normal file
116
cmd/fake-smtp/smtp.go
Normal file
@ -0,0 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/command"
|
||||
"forge.cadoles.com/wpetit/fake-smtp/internal/config"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
ctn *service.Container
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
// Login handles a login command with username and password.
|
||||
func (b *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
||||
if b.username != "" && username != b.username {
|
||||
return nil, errors.New("invalid username")
|
||||
}
|
||||
|
||||
if b.password != "" && password != b.password {
|
||||
return nil, errors.New("invalid password")
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), container.KeyServiceContainer, b.ctn)
|
||||
|
||||
return &Session{ctx, b.ctn, username}, nil
|
||||
}
|
||||
|
||||
// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
|
||||
func (b *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
||||
if b.username != "" || b.password != "" {
|
||||
return nil, smtp.ErrAuthRequired
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// A Session is returned after successful login.
|
||||
type Session struct {
|
||||
ctx context.Context
|
||||
ctn *service.Container
|
||||
username string
|
||||
}
|
||||
|
||||
func (s *Session) Mail(from string, opts smtp.MailOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Rcpt(to string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Data(r io.Reader) error {
|
||||
env, err := enmime.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not read envelope")
|
||||
}
|
||||
|
||||
bus, err := cqrs.From(s.ctn)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve cqrs service")
|
||||
}
|
||||
|
||||
cmd := &command.StoreEmailRequest{
|
||||
Envelope: env,
|
||||
}
|
||||
|
||||
if _, err := bus.Exec(s.ctx, cmd); err != nil {
|
||||
return errors.Wrap(err, "could not exec command")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Reset() {}
|
||||
|
||||
func (s *Session) Logout() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func startSMTPServer(conf *config.Config, ctn *service.Container) {
|
||||
be := &Backend{
|
||||
ctn: ctn,
|
||||
username: conf.SMTP.Username,
|
||||
password: conf.SMTP.Password,
|
||||
}
|
||||
|
||||
s := smtp.NewServer(be)
|
||||
|
||||
s.Addr = conf.SMTP.Address
|
||||
s.Domain = conf.SMTP.Domain
|
||||
s.ReadTimeout = conf.SMTP.ReadTimeout
|
||||
s.WriteTimeout = conf.SMTP.WriteTimeout
|
||||
s.MaxMessageBytes = conf.SMTP.MaxMessageBytes
|
||||
s.MaxRecipients = conf.SMTP.MaxRecipients
|
||||
s.AllowInsecureAuth = conf.SMTP.AllowInsecureAuth
|
||||
if conf.SMTP.Debug {
|
||||
s.Debug = os.Stdout
|
||||
}
|
||||
|
||||
log.Println("starting server at", s.Addr)
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
20
cmd/fake-smtp/template/blocks/base.html.tmpl
Normal file
20
cmd/fake-smtp/template/blocks/base.html.tmpl
Normal file
@ -0,0 +1,20 @@
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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" />
|
||||
{{end}}
|
||||
{{- block "head_script" . -}}
|
||||
<script type="text/javascript" src="/main.js"></script>
|
||||
{{end}}
|
||||
</head>
|
||||
<body>
|
||||
{{- block "body" . -}}{{- end -}}
|
||||
{{- block "body_script" . -}}{{end}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
23
cmd/fake-smtp/template/blocks/flash.html.tmpl
Normal file
23
cmd/fake-smtp/template/blocks/flash.html.tmpl
Normal file
@ -0,0 +1,23 @@
|
||||
{{define "flash"}}
|
||||
<div class="flash has-margin-top-small has-margin-bottom-small">
|
||||
{{- range .Flashes -}}
|
||||
{{- if eq .Type "error" -}}
|
||||
{{template "flash_message" map "Title" "Erreur" "MessageClass" "is-danger" "Message" .Message }}
|
||||
{{- else if eq .Type "warn" -}}
|
||||
{{template "flash_message" map "Title" "Attention" "MessageClass" "is-warning" "Message" .Message }}
|
||||
{{- else if eq .Type "success" -}}
|
||||
{{template "flash_message" map "Title" "Succès" "MessageClass" "is-success" "Message" .Message }}
|
||||
{{- else -}}
|
||||
{{template "flash_message" map "Title" "Information" "MessageClass" "is-info" "Message" .Message }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "flash_message" -}}
|
||||
<div class="message {{.MessageClass}}">
|
||||
<div class="message-body">
|
||||
<span class="has-text-weight-bold">{{.Title}}</span> {{.Message}}
|
||||
</div>
|
||||
</div>
|
||||
{{- end}}
|
7
cmd/fake-smtp/template/blocks/footer.html.tmpl
Normal file
7
cmd/fake-smtp/template/blocks/footer.html.tmpl
Normal file
@ -0,0 +1,7 @@
|
||||
{{define "footer"}}
|
||||
<p class="has-margin-top-small has-text-right is-size-7 has-text-grey">
|
||||
Version: {{ .BuildInfo.ProjectVersion }} -
|
||||
Réf.: {{ .BuildInfo.GitRef }} -
|
||||
Date de construction: {{ .BuildInfo.BuildDate }}
|
||||
</p>
|
||||
{{end}}
|
20
cmd/fake-smtp/template/blocks/header.html.tmpl
Normal file
20
cmd/fake-smtp/template/blocks/header.html.tmpl
Normal file
@ -0,0 +1,20 @@
|
||||
{{define "header"}}
|
||||
<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">
|
||||
{{if or .Emails .Email}}
|
||||
📬
|
||||
{{else}}
|
||||
📭
|
||||
{{end}}
|
||||
Fake<span class="has-text-grey">SMTP</span>
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column"></div>
|
||||
<div class="column is-narrow">
|
||||
{{block "header_buttons" .}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
141
cmd/fake-smtp/template/layouts/email.html.tmpl
Normal file
141
cmd/fake-smtp/template/layouts/email.html.tmpl
Normal file
@ -0,0 +1,141 @@
|
||||
{{define "title"}}Email - FakeSMTP{{end}}
|
||||
{{define "header_buttons"}}
|
||||
<button class="button is-danger"
|
||||
data-controller="restful"
|
||||
data-restful-endpoint="./{{ .Email.ID }}"
|
||||
data-restful-method="DELETE"
|
||||
data-restful-redirect="../">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
{{end}}
|
||||
{{define "body"}}
|
||||
<section class="home is-fullheight section">
|
||||
<div class="container is-fluid">
|
||||
{{template "header" .}}
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h4 class="title is-size-4">Email</h4>
|
||||
{{template "email_head" .}}
|
||||
</div>
|
||||
{{if .Email.Attachments}}
|
||||
<div class="column is-narrow">
|
||||
<h4 class="title is-size-4">Attachments ({{len .Email.Attachments}})</h4>
|
||||
<ul>
|
||||
{{ $email := .Email }}
|
||||
{{range $i, $a := .Email.Attachments}}
|
||||
<li><a href="{{ $email.ID }}/attachments/{{ $i }}" download="{{ $a.Name }}">{{ $a.Name }}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div data-controller="tabs">
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li data-action="click->tabs#openTab" data-target="tabs.tab" data-tabs-name="html" {{if .Email.HTML}}class="is-active"{{end}}><a>HTML</a></li>
|
||||
<li data-action="click->tabs#openTab" data-target="tabs.tab" data-tabs-name="text" {{if not .Email.HTML}}class="is-active"{{end}}><a>Text</a></li>
|
||||
<li data-action="click->tabs#openTab" data-target="tabs.tab" data-tabs-name="headers"><a>Headers</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<iframe data-target="tabs.tabContent" data-tabs-for="html"
|
||||
frameborder="0"
|
||||
data-controller="iframe"
|
||||
data-action="load->iframe#onLoad"
|
||||
style="width:100%;{{if not .Email.HTML}}display:none;{{end}}"
|
||||
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>
|
||||
</div>
|
||||
<div data-target="tabs.tabContent" data-tabs-for="headers" style="display:none">
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
<tr>
|
||||
<thead>
|
||||
<tbody>
|
||||
{{range $k, $v := .Email.Headers}}
|
||||
<tr>
|
||||
<td><code>{{ $k }}</code></td>
|
||||
<td>
|
||||
{{range $v}}
|
||||
<code>{{ . }}</code>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "footer" .}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{define "email_head"}}
|
||||
<div class="columns">
|
||||
<div class="column is-1">
|
||||
<h5 class="is-size-5">From</h5>
|
||||
</div>
|
||||
<div class="column">
|
||||
{{template "email_adresses" .Email.From}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-1">
|
||||
<h5 class="is-size-5">To</h5>
|
||||
</div>
|
||||
<div class="column">
|
||||
{{template "email_adresses" .Email.To}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .Email.Cc }}
|
||||
<div class="columns">
|
||||
<div class="column is-1">
|
||||
<h5 class="is-size-5">Cc</h5>
|
||||
</div>
|
||||
<div class="column">
|
||||
{{template "email_adresses" .Email.Cc}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Email.Cci }}
|
||||
<div class="columns">
|
||||
<div class="column is-1">
|
||||
<h5 class="is-size-5">Cci</h5>
|
||||
</div>
|
||||
<div class="column">
|
||||
{{template "email_adresses" .Email.Cci}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="columns">
|
||||
<div class="column is-1">
|
||||
<h5 class="is-size-5">Subject</h5>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p class="is-size-5">{{.Email.Subject}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "email_adresses"}}
|
||||
{{- range .}}
|
||||
<span class="tag">
|
||||
{{- if .Name -}}
|
||||
{{.Name}} <{{.Address}}>
|
||||
{{- else -}}
|
||||
{{.Address}}
|
||||
{{- end -}}
|
||||
</span>
|
||||
{{- end -}}
|
||||
{{end}}
|
||||
{{template "base" .}}
|
61
cmd/fake-smtp/template/layouts/inbox.html.tmpl
Normal file
61
cmd/fake-smtp/template/layouts/inbox.html.tmpl
Normal file
@ -0,0 +1,61 @@
|
||||
{{define "title"}}Inbox - FakeSMTP{{end}}
|
||||
{{define "header_buttons"}}
|
||||
<button
|
||||
data-controller="restful"
|
||||
data-restful-endpoint="/emails"
|
||||
data-restful-method="DELETE"
|
||||
class="button is-danger">
|
||||
🗑️ Clear
|
||||
</button>
|
||||
{{end}}
|
||||
{{define "body"}}
|
||||
<section class="home is-fullheight section">
|
||||
<div class="container is-fluid">
|
||||
{{template "header" .}}
|
||||
<div>
|
||||
<table class="inbox table is-fullwidth is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="email-subject">Subject</th>
|
||||
<th class="email-from">From</th>
|
||||
<th class="email-to">Recipients</th>
|
||||
<th class="email-sentat">Date</th>
|
||||
<th class="email-actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Emails}}
|
||||
<tr>
|
||||
<td class="email-subject"><div>{{ .Subject }}</div></td>
|
||||
<td class="email-from">
|
||||
{{range .From}}
|
||||
<span class="tag">{{ .Address }}</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="email-to">
|
||||
{{range .To}}
|
||||
<span class="tag">{{ .Address }}</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="email-sentat">
|
||||
<span class="is-size-7">{{ .SentAt.Format "02/01/2006 15:04:05"}}</span>
|
||||
</td>
|
||||
<td class="email-actions">
|
||||
<div class="buttons is-right">
|
||||
<a href="./emails/{{ .ID }}" class="button is-small is-link">👁️ See</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="5" class="has-text-centered">No email yet.</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{template "footer" .}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{template "base" .}}
|
Reference in New Issue
Block a user