Send an email on notification of success payment
To send the actual mail with sendmail, i have stolen the code from go-mail[0] and removed everything i did not need. This is because there is no Go package to send email in Debian 12, and this was easier than to build the DEB for go-mail. Once i have the time…. [0]: https://go-mail.dev/
This commit is contained in:
parent
1179ba9c9a
commit
4a7b0112ef
2
Makefile
2
Makefile
|
@ -1,4 +1,4 @@
|
||||||
HTML_FILES := $(shell find web -name *.gohtml -o -name *.svg)
|
HTML_FILES := $(shell find web -name *.gohtml -o -name *.svg -o -name *.gotxt)
|
||||||
GO_FILES := $(shell find . -name *.go)
|
GO_FILES := $(shell find . -name *.go)
|
||||||
DEFAULT_DOMAIN = camper
|
DEFAULT_DOMAIN = camper
|
||||||
POT_FILE = po/$(DEFAULT_DOMAIN).pot
|
POT_FILE = po/$(DEFAULT_DOMAIN).pot
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoOutWriter is an error message that should be used if a Base64LineBreaker has no out io.Writer set
|
||||||
|
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
|
||||||
|
|
||||||
|
// Base64LineBreaker is an io.WriteCloser that writes Base64 encoded data streams
|
||||||
|
// with line breaks at a given line length
|
||||||
|
type Base64LineBreaker struct {
|
||||||
|
line [MaxBodyLength]byte
|
||||||
|
used int
|
||||||
|
out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
var nl = []byte(SingleNewLine)
|
||||||
|
|
||||||
|
// Write writes the data stream and inserts a SingleNewLine when the maximum
|
||||||
|
// line length is reached
|
||||||
|
func (l *Base64LineBreaker) Write(b []byte) (n int, err error) {
|
||||||
|
if l.out == nil {
|
||||||
|
err = fmt.Errorf(ErrNoOutWriter)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if l.used+len(b) < MaxBodyLength {
|
||||||
|
copy(l.line[l.used:], b)
|
||||||
|
l.used += len(b)
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = l.out.Write(l.line[0:l.used])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
excess := MaxBodyLength - l.used
|
||||||
|
l.used = 0
|
||||||
|
|
||||||
|
n, err = l.out.Write(b[0:excess])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = l.out.Write(nl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.Write(b[excess:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the Base64LineBreaker and writes any access data that is still
|
||||||
|
// unwritten in memory
|
||||||
|
func (l *Base64LineBreaker) Close() (err error) {
|
||||||
|
if l.used > 0 {
|
||||||
|
_, err = l.out.Write(l.line[0:l.used])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = l.out.Write(nl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
// Charset represents a character set for the encoding
|
||||||
|
type Charset string
|
||||||
|
|
||||||
|
// ContentType represents a content type for the Msg
|
||||||
|
type ContentType string
|
||||||
|
|
||||||
|
// Encoding represents a MIME encoding scheme like quoted-printable or Base64.
|
||||||
|
type Encoding string
|
||||||
|
|
||||||
|
// MIMEVersion represents the MIME version for the mail
|
||||||
|
type MIMEVersion string
|
||||||
|
|
||||||
|
// MIMEType represents the MIME type for the mail
|
||||||
|
type MIMEType string
|
||||||
|
|
||||||
|
// List of supported encodings
|
||||||
|
const (
|
||||||
|
// EncodingB64 represents the Base64 encoding as specified in RFC 2045.
|
||||||
|
EncodingB64 Encoding = "base64"
|
||||||
|
|
||||||
|
// EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045.
|
||||||
|
EncodingQP Encoding = "quoted-printable"
|
||||||
|
|
||||||
|
// NoEncoding avoids any character encoding (except of the mail headers)
|
||||||
|
NoEncoding Encoding = "8bit"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CharsetUTF8 represents the "UTF-8" charset
|
||||||
|
CharsetUTF8 Charset = "UTF-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Mime10 is the MIME Version 1.0
|
||||||
|
Mime10 MIMEVersion = "1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeTextPlain ContentType = "text/plain"
|
||||||
|
TypeTextHTML ContentType = "text/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MIMEAlternative MIMEType = "alternative"
|
||||||
|
)
|
||||||
|
|
||||||
|
// String is a standard method to convert an Encoding into a printable format
|
||||||
|
func (e Encoding) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is a standard method to convert a Charset into a printable format
|
||||||
|
func (c Charset) String() string {
|
||||||
|
return string(c)
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
// Header represents a generic mail header field name
|
||||||
|
type Header string
|
||||||
|
|
||||||
|
// AddrHeader represents an address related mail Header field name
|
||||||
|
type AddrHeader string
|
||||||
|
|
||||||
|
// List of common generic header field names
|
||||||
|
const (
|
||||||
|
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header
|
||||||
|
HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
|
||||||
|
|
||||||
|
// HeaderContentType is the "Content-Type" header
|
||||||
|
HeaderContentType Header = "Content-Type"
|
||||||
|
|
||||||
|
// HeaderDate represents the "Date" field
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc822#section-5.1
|
||||||
|
HeaderDate Header = "Date"
|
||||||
|
|
||||||
|
// HeaderMessageID represents the "Message-ID" field for message identification
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5
|
||||||
|
HeaderMessageID Header = "Message-ID"
|
||||||
|
|
||||||
|
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045
|
||||||
|
// See: https://datatracker.ietf.org/doc/html/rfc2045#section-4
|
||||||
|
HeaderMIMEVersion Header = "MIME-Version"
|
||||||
|
|
||||||
|
// HeaderReplyTo is the "Reply-To" header field
|
||||||
|
HeaderReplyTo Header = "Reply-To"
|
||||||
|
|
||||||
|
// HeaderSubject is the "Subject" header field
|
||||||
|
HeaderSubject Header = "Subject"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List of common address header field names
|
||||||
|
const (
|
||||||
|
// HeaderCc is the "Carbon Copy" header field
|
||||||
|
HeaderCc AddrHeader = "Cc"
|
||||||
|
|
||||||
|
// HeaderEnvelopeFrom is the envelope FROM header field
|
||||||
|
// It's not included in the mail body but only used by the Client for the envelope
|
||||||
|
HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom"
|
||||||
|
|
||||||
|
// HeaderFrom is the "From" header field
|
||||||
|
HeaderFrom AddrHeader = "From"
|
||||||
|
|
||||||
|
// HeaderTo is the "Recipient" header field
|
||||||
|
HeaderTo AddrHeader = "To"
|
||||||
|
)
|
|
@ -0,0 +1,362 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
ht "html/template"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net/mail"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
tt "text/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// errTplExecuteFailed is issued when the template execution was not successful
|
||||||
|
errTplExecuteFailed = "failed to execute template: %w"
|
||||||
|
|
||||||
|
// errTplPointerNil is issued when a template pointer is expected but it is nil
|
||||||
|
errTplPointerNil = "template pointer is nil"
|
||||||
|
|
||||||
|
// errParseMailAddr is used when a mail address could not be validated
|
||||||
|
errParseMailAddr = "failed to parse mail address %q: %w"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Msg is the mail message struct
|
||||||
|
type Msg struct {
|
||||||
|
// addrHeader is a slice of strings that the different mail AddrHeader fields
|
||||||
|
addrHeader map[AddrHeader][]*mail.Address
|
||||||
|
|
||||||
|
// boundary is the MIME content boundary
|
||||||
|
boundary string
|
||||||
|
|
||||||
|
// charset represents the charset of the mail (defaults to UTF-8)
|
||||||
|
charset Charset
|
||||||
|
|
||||||
|
// encoder represents a mime.WordEncoder from the std lib
|
||||||
|
encoder mime.WordEncoder
|
||||||
|
|
||||||
|
// encoding represents the message encoding (the encoder will be a corresponding WordEncoder)
|
||||||
|
encoding Encoding
|
||||||
|
|
||||||
|
// genHeader is a slice of strings that the different generic mail Header fields
|
||||||
|
genHeader map[Header][]string
|
||||||
|
|
||||||
|
// mimever represents the MIME version
|
||||||
|
mimever MIMEVersion
|
||||||
|
|
||||||
|
// parts represent the different parts of the Msg
|
||||||
|
parts []*Part
|
||||||
|
|
||||||
|
// preformHeader is a slice of strings that the different generic mail Header fields
|
||||||
|
// of which content is already preformatted and will not be affected by the automatic line
|
||||||
|
// breaks
|
||||||
|
preformHeader map[Header]string
|
||||||
|
|
||||||
|
// sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation
|
||||||
|
sendError error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendmailPath is the default system path to the sendmail binary
|
||||||
|
const SendmailPath = "/usr/sbin/sendmail"
|
||||||
|
|
||||||
|
// MsgOption returns a function that can be used for grouping Msg options
|
||||||
|
type MsgOption func(*Msg)
|
||||||
|
|
||||||
|
// NewMsg returns a new Msg pointer
|
||||||
|
func NewMsg() *Msg {
|
||||||
|
m := &Msg{
|
||||||
|
addrHeader: make(map[AddrHeader][]*mail.Address),
|
||||||
|
charset: CharsetUTF8,
|
||||||
|
encoding: EncodingQP,
|
||||||
|
genHeader: make(map[Header][]string),
|
||||||
|
preformHeader: make(map[Header]string),
|
||||||
|
mimever: Mime10,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the matching mime.WordEncoder for the Msg
|
||||||
|
m.setEncoder()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGenHeader sets a generic header field of the Msg
|
||||||
|
// For adding address headers like "To:" or "From", see SetAddrHeader
|
||||||
|
func (m *Msg) SetGenHeader(h Header, v ...string) {
|
||||||
|
if m.genHeader == nil {
|
||||||
|
m.genHeader = make(map[Header][]string)
|
||||||
|
}
|
||||||
|
for i, hv := range v {
|
||||||
|
v[i] = m.encodeString(hv)
|
||||||
|
}
|
||||||
|
m.genHeader[h] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAddrHeader sets an address related header field of the Msg
|
||||||
|
func (m *Msg) SetAddrHeader(h AddrHeader, v ...string) error {
|
||||||
|
if m.addrHeader == nil {
|
||||||
|
m.addrHeader = make(map[AddrHeader][]*mail.Address)
|
||||||
|
}
|
||||||
|
var al []*mail.Address
|
||||||
|
for _, av := range v {
|
||||||
|
a, err := mail.ParseAddress(av)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errParseMailAddr, av, err)
|
||||||
|
}
|
||||||
|
al = append(al, a)
|
||||||
|
}
|
||||||
|
switch h {
|
||||||
|
case HeaderFrom:
|
||||||
|
if len(al) > 0 {
|
||||||
|
m.addrHeader[h] = []*mail.Address{al[0]}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
m.addrHeader[h] = al
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// From takes and validates a given mail address and sets it as "From" genHeader of the Msg
|
||||||
|
func (m *Msg) From(f string) error {
|
||||||
|
return m.SetAddrHeader(HeaderFrom, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To takes and validates a given mail address list sets the To: addresses of the Msg
|
||||||
|
func (m *Msg) To(t ...string) error {
|
||||||
|
return m.SetAddrHeader(HeaderTo, t...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyTo takes and validates a given mail address and sets it as "Reply-To" addrHeader of the Msg
|
||||||
|
func (m *Msg) ReplyTo(r string) error {
|
||||||
|
rt, err := mail.ParseAddress(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse reply-to address: %w", err)
|
||||||
|
}
|
||||||
|
m.SetGenHeader(HeaderReplyTo, rt.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject sets the "Subject" header field of the Msg
|
||||||
|
func (m *Msg) Subject(s string) {
|
||||||
|
m.SetGenHeader(HeaderSubject, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMessageID generates a random message id for the mail
|
||||||
|
func (m *Msg) SetMessageID() {
|
||||||
|
hn, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
hn = "localhost.localdomain"
|
||||||
|
}
|
||||||
|
rn, _ := randNum(100000000)
|
||||||
|
rm, _ := randNum(10000)
|
||||||
|
rs, _ := randomStringSecure(17)
|
||||||
|
pid := os.Getpid() * rm
|
||||||
|
mid := fmt.Sprintf("%d.%d%d.%s@%s", pid, rn, rm, rs, hn)
|
||||||
|
m.SetMessageIDWithValue(mid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMessageIDWithValue sets the message id for the mail
|
||||||
|
func (m *Msg) SetMessageIDWithValue(v string) {
|
||||||
|
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDate sets the Date genHeader field to the current time in a valid format
|
||||||
|
func (m *Msg) SetDate() {
|
||||||
|
ts := time.Now().Format(time.RFC1123Z)
|
||||||
|
m.SetGenHeader(HeaderDate, ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBodyWriter sets the body of the message.
|
||||||
|
func (m *Msg) SetBodyWriter(ct ContentType, w func(io.Writer) (int64, error)) {
|
||||||
|
p := m.newPart(ct)
|
||||||
|
p.w = w
|
||||||
|
m.parts = []*Part{p}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer
|
||||||
|
// The content type will be set to text/plain automatically
|
||||||
|
func (m *Msg) SetBodyTextTemplate(t *tt.Template, d interface{}) error {
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf(errTplPointerNil)
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
if err := t.Execute(&buf, d); err != nil {
|
||||||
|
return fmt.Errorf(errTplExecuteFailed, err)
|
||||||
|
}
|
||||||
|
w := writeFuncFromBuffer(&buf)
|
||||||
|
m.SetBodyWriter(TypeTextPlain, w)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAlternativeWriter sets the body of the message.
|
||||||
|
func (m *Msg) AddAlternativeWriter(ct ContentType, w func(io.Writer) (int64, error)) {
|
||||||
|
p := m.newPart(ct)
|
||||||
|
p.w = w
|
||||||
|
m.parts = append(m.parts, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output
|
||||||
|
// The content type will be set to text/html automatically
|
||||||
|
func (m *Msg) AddAlternativeHTMLTemplate(t *ht.Template, d interface{}) error {
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf(errTplPointerNil)
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
if err := t.Execute(&buf, d); err != nil {
|
||||||
|
return fmt.Errorf(errTplExecuteFailed, err)
|
||||||
|
}
|
||||||
|
w := writeFuncFromBuffer(&buf)
|
||||||
|
m.AddAlternativeWriter(TypeTextHTML, w)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo writes the formatted Msg into a give io.Writer and satisfies the io.WriteTo interface
|
||||||
|
func (m *Msg) WriteTo(w io.Writer) (int64, error) {
|
||||||
|
mw := &msgWriter{w: w, c: m.charset, en: m.encoder}
|
||||||
|
mw.writeMsg(m)
|
||||||
|
return mw.n, mw.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path
|
||||||
|
func (m *Msg) WriteToSendmail() error {
|
||||||
|
return m.WriteToSendmailWithCommand(SendmailPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToSendmailWithCommand returns WriteToSendmailWithContext with a default timeout
|
||||||
|
// of 5 seconds and a given sendmail path
|
||||||
|
func (m *Msg) WriteToSendmailWithCommand(sp string) error {
|
||||||
|
tctx, tcfn := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
defer tcfn()
|
||||||
|
return m.WriteToSendmailWithContext(tctx, sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToSendmailWithContext opens a pipe to the local sendmail binary and tries to send the
|
||||||
|
// mail though that. It takes a context.Context, the path to the sendmail binary and additional
|
||||||
|
// arguments for the sendmail binary as parameters
|
||||||
|
func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sp string, a ...string) error {
|
||||||
|
ec := exec.CommandContext(ctx, sp)
|
||||||
|
ec.Args = append(ec.Args, "-oi", "-t")
|
||||||
|
ec.Args = append(ec.Args, a...)
|
||||||
|
|
||||||
|
se, err := ec.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set STDERR pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := ec.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set STDIN pipe: %w", err)
|
||||||
|
}
|
||||||
|
if se == nil || si == nil {
|
||||||
|
return fmt.Errorf("received nil for STDERR or STDIN pipe")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the execution and write to STDIN
|
||||||
|
if err = ec.Start(); err != nil {
|
||||||
|
return fmt.Errorf("could not start sendmail execution: %w", err)
|
||||||
|
}
|
||||||
|
_, err = m.WriteTo(si)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, syscall.EPIPE) {
|
||||||
|
return fmt.Errorf("failed to write mail to buffer: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close STDIN and wait for completion or cancellation of the sendmail executable
|
||||||
|
if err = si.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close STDIN pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the stderr pipe for possible errors
|
||||||
|
serr, err := io.ReadAll(se)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read STDERR pipe: %w", err)
|
||||||
|
}
|
||||||
|
if len(serr) > 0 {
|
||||||
|
return fmt.Errorf("sendmail command failed: %s", string(serr))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ec.Wait(); err != nil {
|
||||||
|
return fmt.Errorf("sendmail command execution failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendError returns the sendError field of the Msg
|
||||||
|
func (m *Msg) SendError() error {
|
||||||
|
return m.sendError
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeString encodes a string based on the configured message encoder and the corresponding
|
||||||
|
// charset for the Msg
|
||||||
|
func (m *Msg) encodeString(s string) string {
|
||||||
|
return m.encoder.Encode(string(m.charset), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasAlt returns true if the Msg has more than one part
|
||||||
|
func (m *Msg) hasAlt() bool {
|
||||||
|
c := 0
|
||||||
|
for _, p := range m.parts {
|
||||||
|
if !p.del {
|
||||||
|
c++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPart returns a new Part for the Msg
|
||||||
|
func (m *Msg) newPart(ct ContentType) *Part {
|
||||||
|
return &Part{
|
||||||
|
ctype: ct,
|
||||||
|
cset: m.charset,
|
||||||
|
enc: m.encoding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message
|
||||||
|
func (m *Msg) setEncoder() {
|
||||||
|
m.encoder = getEncoder(m.encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDefaultHeader sets some default headers, if they haven't been set before
|
||||||
|
func (m *Msg) addDefaultHeader() {
|
||||||
|
if _, ok := m.genHeader[HeaderDate]; !ok {
|
||||||
|
m.SetDate()
|
||||||
|
}
|
||||||
|
if _, ok := m.genHeader[HeaderMessageID]; !ok {
|
||||||
|
m.SetMessageID()
|
||||||
|
}
|
||||||
|
m.SetGenHeader(HeaderMIMEVersion, string(m.mimever))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
|
||||||
|
func getEncoder(e Encoding) mime.WordEncoder {
|
||||||
|
switch e {
|
||||||
|
case EncodingQP:
|
||||||
|
return mime.QEncoding
|
||||||
|
case EncodingB64:
|
||||||
|
return mime.BEncoding
|
||||||
|
default:
|
||||||
|
return mime.QEncoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFuncFromBuffer is a common method to convert a byte buffer into a writeFunc as
|
||||||
|
// often required by this library
|
||||||
|
func writeFuncFromBuffer(buf *bytes.Buffer) func(io.Writer) (int64, error) {
|
||||||
|
w := func(w io.Writer) (int64, error) {
|
||||||
|
nb, err := w.Write(buf.Bytes())
|
||||||
|
return int64(nb), err
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
|
@ -0,0 +1,282 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"net/textproto"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxHeaderLength defines the maximum line length for a mail header
|
||||||
|
// RFC 2047 suggests 76 characters
|
||||||
|
const MaxHeaderLength = 76
|
||||||
|
|
||||||
|
// MaxBodyLength defines the maximum line length for the mail body
|
||||||
|
// RFC 2047 suggests 76 characters
|
||||||
|
const MaxBodyLength = 76
|
||||||
|
|
||||||
|
// SingleNewLine represents a new line that can be used by the msgWriter to issue a carriage return
|
||||||
|
const SingleNewLine = "\r\n"
|
||||||
|
|
||||||
|
// DoubleNewLine represents a double new line that can be used by the msgWriter to
|
||||||
|
// indicate a new segment of the mail
|
||||||
|
const DoubleNewLine = "\r\n\r\n"
|
||||||
|
|
||||||
|
// msgWriter handles the I/O to the io.WriteCloser of the SMTP client
|
||||||
|
type msgWriter struct {
|
||||||
|
c Charset
|
||||||
|
d int8
|
||||||
|
en mime.WordEncoder
|
||||||
|
err error
|
||||||
|
mpw [3]*multipart.Writer
|
||||||
|
n int64
|
||||||
|
pw io.Writer
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements the io.Writer interface for msgWriter
|
||||||
|
func (mw *msgWriter) Write(p []byte) (int, error) {
|
||||||
|
if mw.err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
n, mw.err = mw.w.Write(p)
|
||||||
|
mw.n += int64(n)
|
||||||
|
return n, mw.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeMsg formats the message and sends it to its io.Writer
|
||||||
|
func (mw *msgWriter) writeMsg(m *Msg) {
|
||||||
|
m.addDefaultHeader()
|
||||||
|
mw.writeGenHeader(m)
|
||||||
|
mw.writePreformattedGenHeader(m)
|
||||||
|
|
||||||
|
// Set the FROM header (or envelope FROM if FROM is empty)
|
||||||
|
hf := true
|
||||||
|
f, ok := m.addrHeader[HeaderFrom]
|
||||||
|
if !ok || (len(f) == 0 || f == nil) {
|
||||||
|
f, ok = m.addrHeader[HeaderEnvelopeFrom]
|
||||||
|
if !ok || (len(f) == 0 || f == nil) {
|
||||||
|
hf = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hf && (len(f) > 0 && f[0] != nil) {
|
||||||
|
mw.writeHeader(Header(HeaderFrom), f[0].String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the rest of the address headers
|
||||||
|
for _, t := range []AddrHeader{HeaderTo, HeaderCc} {
|
||||||
|
if al, ok := m.addrHeader[t]; ok {
|
||||||
|
var v []string
|
||||||
|
for _, a := range al {
|
||||||
|
v = append(v, a.String())
|
||||||
|
}
|
||||||
|
mw.writeHeader(Header(t), v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.hasAlt() {
|
||||||
|
mw.startMP(MIMEAlternative, m.boundary)
|
||||||
|
mw.writeString(DoubleNewLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range m.parts {
|
||||||
|
if !p.del {
|
||||||
|
mw.writePart(p, m.charset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.hasAlt() {
|
||||||
|
mw.stopMP()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeGenHeader writes out all generic headers to the msgWriter
|
||||||
|
func (mw *msgWriter) writeGenHeader(m *Msg) {
|
||||||
|
gk := make([]string, 0, len(m.genHeader))
|
||||||
|
for k := range m.genHeader {
|
||||||
|
gk = append(gk, string(k))
|
||||||
|
}
|
||||||
|
sort.Strings(gk)
|
||||||
|
for _, k := range gk {
|
||||||
|
mw.writeHeader(Header(k), m.genHeader[Header(k)]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePreformattedHeader writes out all preformatted generic headers to the msgWriter
|
||||||
|
func (mw *msgWriter) writePreformattedGenHeader(m *Msg) {
|
||||||
|
for k, v := range m.preformHeader {
|
||||||
|
mw.writeString(fmt.Sprintf("%s: %s%s", k, v, SingleNewLine))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startMP writes a multipart beginning
|
||||||
|
func (mw *msgWriter) startMP(mt MIMEType, b string) {
|
||||||
|
mp := multipart.NewWriter(mw)
|
||||||
|
if b != "" {
|
||||||
|
mw.err = mp.SetBoundary(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
ct := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mt, mp.Boundary())
|
||||||
|
mw.mpw[mw.d] = mp
|
||||||
|
|
||||||
|
if mw.d == 0 {
|
||||||
|
mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, ct))
|
||||||
|
}
|
||||||
|
if mw.d > 0 {
|
||||||
|
mw.newPart(map[string][]string{"Content-Type": {ct}})
|
||||||
|
}
|
||||||
|
mw.d++
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopMP closes the multipart
|
||||||
|
func (mw *msgWriter) stopMP() {
|
||||||
|
if mw.d > 0 {
|
||||||
|
mw.err = mw.mpw[mw.d-1].Close()
|
||||||
|
mw.d--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPart creates a new MIME multipart io.Writer and sets the part writer to it
|
||||||
|
func (mw *msgWriter) newPart(h map[string][]string) {
|
||||||
|
mw.pw, mw.err = mw.mpw[mw.d-1].CreatePart(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePart writes the corresponding part to the Msg body
|
||||||
|
func (mw *msgWriter) writePart(p *Part, cs Charset) {
|
||||||
|
pcs := p.cset
|
||||||
|
if pcs.String() == "" {
|
||||||
|
pcs = cs
|
||||||
|
}
|
||||||
|
ct := fmt.Sprintf("%s; charset=%s", p.ctype, pcs)
|
||||||
|
cte := p.enc.String()
|
||||||
|
if mw.d == 0 {
|
||||||
|
mw.writeHeader(HeaderContentType, ct)
|
||||||
|
mw.writeHeader(HeaderContentTransferEnc, cte)
|
||||||
|
mw.writeString(SingleNewLine)
|
||||||
|
}
|
||||||
|
if mw.d > 0 {
|
||||||
|
mh := textproto.MIMEHeader{}
|
||||||
|
mh.Add(string(HeaderContentType), ct)
|
||||||
|
mh.Add(string(HeaderContentTransferEnc), cte)
|
||||||
|
mw.newPart(mh)
|
||||||
|
}
|
||||||
|
mw.writeBody(p.w, p.enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeString writes a string into the msgWriter's io.Writer interface
|
||||||
|
func (mw *msgWriter) writeString(s string) {
|
||||||
|
if mw.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
n, mw.err = io.WriteString(mw.w, s)
|
||||||
|
mw.n += int64(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeHeader writes a header into the msgWriter's io.Writer
|
||||||
|
func (mw *msgWriter) writeHeader(k Header, vl ...string) {
|
||||||
|
wbuf := bytes.Buffer{}
|
||||||
|
cl := MaxHeaderLength - 2
|
||||||
|
wbuf.WriteString(string(k))
|
||||||
|
cl -= len(k)
|
||||||
|
if len(vl) == 0 {
|
||||||
|
wbuf.WriteString(":\r\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wbuf.WriteString(": ")
|
||||||
|
cl -= 2
|
||||||
|
|
||||||
|
fs := strings.Join(vl, ", ")
|
||||||
|
sfs := strings.Split(fs, " ")
|
||||||
|
for i, v := range sfs {
|
||||||
|
if cl-len(v) <= 1 {
|
||||||
|
wbuf.WriteString(fmt.Sprintf("%s ", SingleNewLine))
|
||||||
|
cl = MaxHeaderLength - 3
|
||||||
|
}
|
||||||
|
wbuf.WriteString(v)
|
||||||
|
if i < len(sfs)-1 {
|
||||||
|
wbuf.WriteString(" ")
|
||||||
|
cl -= 1
|
||||||
|
}
|
||||||
|
cl -= len(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
bufs := wbuf.String()
|
||||||
|
bufs = strings.ReplaceAll(bufs, fmt.Sprintf(" %s", SingleNewLine), SingleNewLine)
|
||||||
|
mw.writeString(bufs)
|
||||||
|
mw.writeString("\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBody writes an io.Reader into an io.Writer using provided Encoding
|
||||||
|
func (mw *msgWriter) writeBody(f func(io.Writer) (int64, error), e Encoding) {
|
||||||
|
var w io.Writer
|
||||||
|
var ew io.WriteCloser
|
||||||
|
var n int64
|
||||||
|
var err error
|
||||||
|
if mw.d == 0 {
|
||||||
|
w = mw.w
|
||||||
|
}
|
||||||
|
if mw.d > 0 {
|
||||||
|
w = mw.pw
|
||||||
|
}
|
||||||
|
wbuf := bytes.Buffer{}
|
||||||
|
lb := Base64LineBreaker{}
|
||||||
|
lb.out = &wbuf
|
||||||
|
|
||||||
|
switch e {
|
||||||
|
case EncodingQP:
|
||||||
|
ew = quotedprintable.NewWriter(&wbuf)
|
||||||
|
case EncodingB64:
|
||||||
|
ew = base64.NewEncoder(base64.StdEncoding, &lb)
|
||||||
|
case NoEncoding:
|
||||||
|
_, err = f(&wbuf)
|
||||||
|
if err != nil {
|
||||||
|
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
||||||
|
}
|
||||||
|
n, err = io.Copy(w, &wbuf)
|
||||||
|
if err != nil && mw.err == nil {
|
||||||
|
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
|
||||||
|
}
|
||||||
|
if mw.d == 0 {
|
||||||
|
mw.n += n
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
ew = quotedprintable.NewWriter(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f(ew)
|
||||||
|
if err != nil {
|
||||||
|
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
||||||
|
}
|
||||||
|
err = ew.Close()
|
||||||
|
if err != nil && mw.err == nil {
|
||||||
|
mw.err = fmt.Errorf("bodyWriter close encoded writer: %w", err)
|
||||||
|
}
|
||||||
|
err = lb.Close()
|
||||||
|
if err != nil && mw.err == nil {
|
||||||
|
mw.err = fmt.Errorf("bodyWriter close linebreaker: %w", err)
|
||||||
|
}
|
||||||
|
n, err = io.Copy(w, &wbuf)
|
||||||
|
if err != nil && mw.err == nil {
|
||||||
|
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the part writer uses the WriteTo() method, we don't need to add the
|
||||||
|
// bytes twice
|
||||||
|
if mw.d == 0 {
|
||||||
|
mw.n += n
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Part is a part of the Msg
|
||||||
|
type Part struct {
|
||||||
|
ctype ContentType
|
||||||
|
cset Charset
|
||||||
|
enc Encoding
|
||||||
|
del bool
|
||||||
|
w func(io.Writer) (int64, error)
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Range of characters for the secure string generation
|
||||||
|
const cr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||||
|
|
||||||
|
// Bitmask sizes for the string generators (based on 93 chars total)
|
||||||
|
const (
|
||||||
|
letterIdxBits = 7 // 7 bits to represent a letter index
|
||||||
|
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||||
|
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||||
|
)
|
||||||
|
|
||||||
|
// randomStringSecure returns a random, n long string of characters. The character set is based
|
||||||
|
// on the s (special chars) and h (human-readable) boolean arguments. This method uses the
|
||||||
|
// crypto/random package and therefore is cryptographically secure
|
||||||
|
func randomStringSecure(n int) (string, error) {
|
||||||
|
rs := strings.Builder{}
|
||||||
|
rs.Grow(n)
|
||||||
|
crl := len(cr)
|
||||||
|
|
||||||
|
rp := make([]byte, 8)
|
||||||
|
_, err := rand.Read(rp)
|
||||||
|
if err != nil {
|
||||||
|
return rs.String(), err
|
||||||
|
}
|
||||||
|
for i, c, r := n-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; {
|
||||||
|
if r == 0 {
|
||||||
|
_, err := rand.Read(rp)
|
||||||
|
if err != nil {
|
||||||
|
return rs.String(), err
|
||||||
|
}
|
||||||
|
c, r = binary.BigEndian.Uint64(rp), letterIdxMax
|
||||||
|
}
|
||||||
|
if idx := int(c & letterIdxMask); idx < crl {
|
||||||
|
rs.WriteByte(cr[idx])
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
c >>= letterIdxBits
|
||||||
|
r--
|
||||||
|
}
|
||||||
|
|
||||||
|
return rs.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// randNum returns a random number with a maximum value of n
|
||||||
|
func randNum(n int) (int, error) {
|
||||||
|
if n <= 0 {
|
||||||
|
return 0, fmt.Errorf("provided number is <= 0: %d", n)
|
||||||
|
}
|
||||||
|
mbi := big.NewInt(int64(n))
|
||||||
|
if !mbi.IsUint64() {
|
||||||
|
return 0, fmt.Errorf("big.NewInt() generation returned negative value: %d", mbi)
|
||||||
|
}
|
||||||
|
rn64, err := rand.Int(rand.Reader, mbi)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
rn := int(rn64.Int64())
|
||||||
|
if rn < 0 {
|
||||||
|
return 0, fmt.Errorf("generated random number does not fit as int64: %d", rn64)
|
||||||
|
}
|
||||||
|
return rn, nil
|
||||||
|
}
|
|
@ -3,23 +3,23 @@ package payment
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
ht "html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
tt "text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/mail"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/redsys"
|
"dev.tandem.ws/tandem/camper/pkg/redsys"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StatusDraft = "draft"
|
|
||||||
StatusPending = "pending"
|
|
||||||
StatusFailed = "failed"
|
|
||||||
StatusCompleted = "completed"
|
StatusCompleted = "completed"
|
||||||
StatusRefunded = "refunded"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublicHandler struct {
|
type PublicHandler struct {
|
||||||
|
@ -249,6 +249,7 @@ func handleNotification(w http.ResponseWriter, r *http.Request, user *auth.User,
|
||||||
}
|
}
|
||||||
switch status {
|
switch status {
|
||||||
case StatusCompleted:
|
case StatusCompleted:
|
||||||
|
_ = sendEmail(r.Context(), conn, payment, company, user.Locale) /* shrug */
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
@ -259,3 +260,109 @@ func handleNotification(w http.ResponseWriter, r *http.Request, user *auth.User,
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CompletedEmail struct {
|
||||||
|
CurrentLocale string
|
||||||
|
CustomerFullName string
|
||||||
|
PaymentReference string
|
||||||
|
AccommodationName string
|
||||||
|
ArrivalDate string
|
||||||
|
DepartureDate string
|
||||||
|
Total string
|
||||||
|
CompanyAddress *address
|
||||||
|
}
|
||||||
|
|
||||||
|
type address struct {
|
||||||
|
TradeName string
|
||||||
|
Address string
|
||||||
|
PostalCode string
|
||||||
|
Province string
|
||||||
|
City string
|
||||||
|
Country string
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendEmail(ctx context.Context, conn *database.Conn, payment *Payment, company *auth.Company, locale *locale.Locale) error {
|
||||||
|
email := &CompletedEmail{
|
||||||
|
CurrentLocale: locale.Language.String(),
|
||||||
|
PaymentReference: payment.OrderNumber(),
|
||||||
|
Total: template.FormatPrice(payment.Total, locale.Language, locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol),
|
||||||
|
CompanyAddress: &address{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromAddress string
|
||||||
|
var toAddress string
|
||||||
|
if err := conn.QueryRow(ctx, `
|
||||||
|
select company.email::text
|
||||||
|
, customer.email::text
|
||||||
|
, customer.full_name
|
||||||
|
, coalesce(i18n.name, campsite_type.name)
|
||||||
|
, to_char(arrival_date, 'DD/MM/YYYY')
|
||||||
|
, to_char(departure_date, 'DD/MM/YYYY')
|
||||||
|
, company.trade_name
|
||||||
|
, company.address
|
||||||
|
, company.postal_code
|
||||||
|
, company.province
|
||||||
|
, company.city
|
||||||
|
, coalesce(country_i18n.name, country.name) as country_name
|
||||||
|
from payment
|
||||||
|
join payment_customer as customer using (payment_id)
|
||||||
|
join campsite_type using (campsite_type_id)
|
||||||
|
left join campsite_type_i18n as i18n on i18n.campsite_type_id = campsite_type.campsite_type_id and i18n.lang_tag = $2
|
||||||
|
join company on company.company_id = payment.company_id
|
||||||
|
join country on country.country_code = company.country_code
|
||||||
|
left join country_i18n on country.country_code = country_i18n.country_code and country_i18n.lang_tag = $2
|
||||||
|
where payment_id = $1
|
||||||
|
`, payment.ID, locale.Language).Scan(&fromAddress,
|
||||||
|
&toAddress,
|
||||||
|
&email.CustomerFullName,
|
||||||
|
&email.AccommodationName,
|
||||||
|
&email.ArrivalDate,
|
||||||
|
&email.DepartureDate,
|
||||||
|
&email.CompanyAddress.TradeName,
|
||||||
|
&email.CompanyAddress.Address,
|
||||||
|
&email.CompanyAddress.PostalCode,
|
||||||
|
&email.CompanyAddress.Province,
|
||||||
|
&email.CompanyAddress.City,
|
||||||
|
&email.CompanyAddress.Country,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := mail.NewMsg()
|
||||||
|
if err := m.From(fromAddress); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.ReplyTo(fromAddress); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.To(toAddress); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Subject(locale.Pgettext("Booking payment successfully received", "subject"))
|
||||||
|
|
||||||
|
baseTemplate := "body.go"
|
||||||
|
baseFilename := "web/templates/mail/payment/" + baseTemplate
|
||||||
|
body, err := tt.New(baseTemplate + "txt").Funcs(tt.FuncMap{
|
||||||
|
"gettext": locale.Get,
|
||||||
|
"pgettext": locale.GetC,
|
||||||
|
}).ParseFiles(baseFilename + "txt")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.SetBodyTextTemplate(body, email); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
alternative, err := ht.New(baseTemplate + "html").Funcs(tt.FuncMap{
|
||||||
|
"gettext": locale.Get,
|
||||||
|
"pgettext": locale.GetC,
|
||||||
|
"raw": func(s string) ht.HTML { return ht.HTML(s) },
|
||||||
|
}).ParseFiles(baseFilename + "html")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.AddAlternativeHTMLTemplate(alternative, email); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.WriteToSendmail()
|
||||||
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
|
||||||
return humanizeBytes(bytes)
|
return humanizeBytes(bytes)
|
||||||
},
|
},
|
||||||
"formatPrice": func(price string) string {
|
"formatPrice": func(price string) string {
|
||||||
return formatPrice(price, user.Locale.Language, user.Locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol)
|
return FormatPrice(price, user.Locale.Language, user.Locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol)
|
||||||
},
|
},
|
||||||
"formatDate": func(time time.Time) template.HTML {
|
"formatDate": func(time time.Time) template.HTML {
|
||||||
return template.HTML(`<time datetime="` + time.Format(database.ISODateFormat) + `">` + time.Format("02/01/2006") + "</time>")
|
return template.HTML(`<time datetime="` + time.Format(database.ISODateFormat) + `">` + time.Format("02/01/2006") + "</time>")
|
||||||
|
@ -156,7 +156,7 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatPrice(price string, language language.Tag, currencyPattern string, decimalDigits int, currencySymbol string) string {
|
func FormatPrice(price string, language language.Tag, currencyPattern string, decimalDigits int, currencySymbol string) string {
|
||||||
p := message.NewPrinter(language)
|
p := message.NewPrinter(language)
|
||||||
f, err := strconv.ParseFloat(price, 64)
|
f, err := strconv.ParseFloat(price, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
69
po/ca.po
69
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: camper\n"
|
"Project-Id-Version: camper\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2024-02-12 17:49+0100\n"
|
"POT-Creation-Date: 2024-02-13 05:09+0100\n"
|
||||||
"PO-Revision-Date: 2024-02-06 10:04+0100\n"
|
"PO-Revision-Date: 2024-02-06 10:04+0100\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||||
|
@ -59,6 +59,66 @@ msgctxt "tooltip"
|
||||||
msgid "Zone 1"
|
msgid "Zone 1"
|
||||||
msgstr "Zona 1"
|
msgstr "Zona 1"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:6
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Booking Payment Notification"
|
||||||
|
msgstr "Notificació de pagament de reserva"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:33
|
||||||
|
#: web/templates/mail/payment/body.gotxt:1
|
||||||
|
msgid "Hi %s,"
|
||||||
|
msgstr "Hola %s,"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:35
|
||||||
|
#: web/templates/mail/payment/body.gotxt:3
|
||||||
|
msgid "We have successfully received the payment for the booking with the following details:"
|
||||||
|
msgstr "Hem rebut amb èxit el pagament de la reserva amb els següents detalls:"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:37
|
||||||
|
msgid "Payment reference: <strong>%s</strong>"
|
||||||
|
msgstr "Referència de pagament: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:39
|
||||||
|
msgid "Accommodation: <strong>%s</strong>"
|
||||||
|
msgstr "Allotjament: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:41
|
||||||
|
msgid "Arrival Date: <strong>%s</strong>"
|
||||||
|
msgstr "Data d’arribada: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:43
|
||||||
|
msgid "Departure Date: <strong>%s</strong>"
|
||||||
|
msgstr "Data de sortida: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:45
|
||||||
|
msgid "Total: <strong>%s</strong>"
|
||||||
|
msgstr "Total: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:48
|
||||||
|
#: web/templates/mail/payment/body.gotxt:11
|
||||||
|
msgid "Thank you for your booking, and see you soon!"
|
||||||
|
msgstr "Moltes gràcies per la reserva i fins aviat!"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:5
|
||||||
|
msgid "Payment reference: **%s**"
|
||||||
|
msgstr "Referència de pagament: **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:6
|
||||||
|
msgid "Accommodation: **%s**"
|
||||||
|
msgstr "Allotjament: **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:7
|
||||||
|
msgid "Arrival Date: **%s**"
|
||||||
|
msgstr "Data d’arribada: **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:8
|
||||||
|
msgid "Departure Date: **%s**"
|
||||||
|
msgstr "Data de sortida: **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:9
|
||||||
|
msgid "Total: **%s**"
|
||||||
|
msgstr "Total: **%s**"
|
||||||
|
|
||||||
#: web/templates/public/payment/success.gohtml:6
|
#: web/templates/public/payment/success.gohtml:6
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Payment Successful"
|
msgid "Payment Successful"
|
||||||
|
@ -1979,11 +2039,16 @@ msgstr "Estat"
|
||||||
msgid "No booking found."
|
msgid "No booking found."
|
||||||
msgstr "No s’ha trobat cap reserva."
|
msgstr "No s’ha trobat cap reserva."
|
||||||
|
|
||||||
#: pkg/payment/public.go:97
|
#: pkg/payment/public.go:107
|
||||||
msgctxt "order product name"
|
msgctxt "order product name"
|
||||||
msgid "Campsite Booking"
|
msgid "Campsite Booking"
|
||||||
msgstr "Reserva de càmping"
|
msgstr "Reserva de càmping"
|
||||||
|
|
||||||
|
#: pkg/payment/public.go:344
|
||||||
|
msgctxt "subject"
|
||||||
|
msgid "Booking payment successfully received"
|
||||||
|
msgstr "Rebut amb èxit el pagament de la reserva"
|
||||||
|
|
||||||
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
|
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
|
||||||
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
||||||
#: pkg/campsite/feature.go:269 pkg/season/admin.go:412
|
#: pkg/campsite/feature.go:269 pkg/season/admin.go:412
|
||||||
|
|
69
po/es.po
69
po/es.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: camper\n"
|
"Project-Id-Version: camper\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2024-02-12 17:49+0100\n"
|
"POT-Creation-Date: 2024-02-13 05:09+0100\n"
|
||||||
"PO-Revision-Date: 2024-02-06 10:04+0100\n"
|
"PO-Revision-Date: 2024-02-06 10:04+0100\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||||
|
@ -59,6 +59,66 @@ msgctxt "tooltip"
|
||||||
msgid "Zone 1"
|
msgid "Zone 1"
|
||||||
msgstr "Zona 1"
|
msgstr "Zona 1"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:6
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Booking Payment Notification"
|
||||||
|
msgstr "Notificación de pago de reserva"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:33
|
||||||
|
#: web/templates/mail/payment/body.gotxt:1
|
||||||
|
msgid "Hi %s,"
|
||||||
|
msgstr "Hola %s,"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:35
|
||||||
|
#: web/templates/mail/payment/body.gotxt:3
|
||||||
|
msgid "We have successfully received the payment for the booking with the following details:"
|
||||||
|
msgstr "Hemos recibido correctamente el pago de la reserva con los siguientes detalles:"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:37
|
||||||
|
msgid "Payment reference: <strong>%s</strong>"
|
||||||
|
msgstr "Referencia del pago: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:39
|
||||||
|
msgid "Accommodation: <strong>%s</strong>"
|
||||||
|
msgstr "Alojamiento: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:41
|
||||||
|
msgid "Arrival Date: <strong>%s</strong>"
|
||||||
|
msgstr "Fecha de llegada: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:43
|
||||||
|
msgid "Departure Date: <strong>%s</strong>"
|
||||||
|
msgstr "Fecha de salida: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:45
|
||||||
|
msgid "Total: <strong>%s</strong>"
|
||||||
|
msgstr "Total: <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:48
|
||||||
|
#: web/templates/mail/payment/body.gotxt:11
|
||||||
|
msgid "Thank you for your booking, and see you soon!"
|
||||||
|
msgstr "Gracias por su reserva y ¡hasta pronto!"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:5
|
||||||
|
msgid "Payment reference: **%s**"
|
||||||
|
msgstr "Referencia del pago: **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:6
|
||||||
|
msgid "Accommodation: **%s**"
|
||||||
|
msgstr "Alojamiento: **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:7
|
||||||
|
msgid "Arrival Date: **%s**"
|
||||||
|
msgstr "Fecha de llegada: **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:8
|
||||||
|
msgid "Departure Date: **%s**"
|
||||||
|
msgstr "Fecha de salida: **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:9
|
||||||
|
msgid "Total: **%s**"
|
||||||
|
msgstr "Total: **%s**"
|
||||||
|
|
||||||
#: web/templates/public/payment/success.gohtml:6
|
#: web/templates/public/payment/success.gohtml:6
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Payment Successful"
|
msgid "Payment Successful"
|
||||||
|
@ -1979,11 +2039,16 @@ msgstr "Estado"
|
||||||
msgid "No booking found."
|
msgid "No booking found."
|
||||||
msgstr "No se ha encontrado ninguna reserva."
|
msgstr "No se ha encontrado ninguna reserva."
|
||||||
|
|
||||||
#: pkg/payment/public.go:97
|
#: pkg/payment/public.go:107
|
||||||
msgctxt "order product name"
|
msgctxt "order product name"
|
||||||
msgid "Campsite Booking"
|
msgid "Campsite Booking"
|
||||||
msgstr "Reserva de camping"
|
msgstr "Reserva de camping"
|
||||||
|
|
||||||
|
#: pkg/payment/public.go:344
|
||||||
|
msgctxt "subject"
|
||||||
|
msgid "Booking payment successfully received"
|
||||||
|
msgstr "Se ha recibido correctamente el pago de la reserva"
|
||||||
|
|
||||||
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
|
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
|
||||||
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
||||||
#: pkg/campsite/feature.go:269 pkg/season/admin.go:412
|
#: pkg/campsite/feature.go:269 pkg/season/admin.go:412
|
||||||
|
|
69
po/fr.po
69
po/fr.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: camper\n"
|
"Project-Id-Version: camper\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2024-02-12 17:49+0100\n"
|
"POT-Creation-Date: 2024-02-13 05:02+0100\n"
|
||||||
"PO-Revision-Date: 2024-02-06 10:05+0100\n"
|
"PO-Revision-Date: 2024-02-06 10:05+0100\n"
|
||||||
"Last-Translator: Oriol Carbonell <info@oriolcarbonell.cat>\n"
|
"Last-Translator: Oriol Carbonell <info@oriolcarbonell.cat>\n"
|
||||||
"Language-Team: French <traduc@traduc.org>\n"
|
"Language-Team: French <traduc@traduc.org>\n"
|
||||||
|
@ -59,6 +59,66 @@ msgctxt "tooltip"
|
||||||
msgid "Zone 1"
|
msgid "Zone 1"
|
||||||
msgstr "Zone 1"
|
msgstr "Zone 1"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:6
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Booking Payment Notification"
|
||||||
|
msgstr "Notification de paiement de réservation"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:33
|
||||||
|
#: web/templates/mail/payment/body.gotxt:1
|
||||||
|
msgid "Hi %s,"
|
||||||
|
msgstr "Salut %s,"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:35
|
||||||
|
#: web/templates/mail/payment/body.gotxt:3
|
||||||
|
msgid "We have successfully received the payment for the booking with the following details:"
|
||||||
|
msgstr "Nous avons reçu avec succès le paiement de la réservation avec les détails suivants :"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:37
|
||||||
|
msgid "Payment reference: <strong>%s</strong>"
|
||||||
|
msgstr "Référence de paiement : <strong>%s</strong>."
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:39
|
||||||
|
msgid "Accommodation: <strong>%s</strong>"
|
||||||
|
msgstr "Hébergement : < strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:41
|
||||||
|
msgid "Arrival Date: <strong>%s</strong>"
|
||||||
|
msgstr "Date d’arrivée : <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:43
|
||||||
|
msgid "Departure Date: <strong>%s</strong>"
|
||||||
|
msgstr "Date de depart : <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:45
|
||||||
|
msgid "Total: <strong>%s</strong>"
|
||||||
|
msgstr "Totale : <strong>%s</strong>"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gohtml:48
|
||||||
|
#: web/templates/mail/payment/body.gotxt:11
|
||||||
|
msgid "Thank you for your booking, and see you soon!"
|
||||||
|
msgstr "Merci pour votre réservation et à bientôt !"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:5
|
||||||
|
msgid "Payment reference: **%s**"
|
||||||
|
msgstr "Référence de paiement : **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:6
|
||||||
|
msgid "Accommodation: **%s**"
|
||||||
|
msgstr "Hébergement : **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:7
|
||||||
|
msgid "Arrival Date: **%s**"
|
||||||
|
msgstr "Date d’arrivée : **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:8
|
||||||
|
msgid "Departure Date: **%s**"
|
||||||
|
msgstr "Date de depart : **%s**"
|
||||||
|
|
||||||
|
#: web/templates/mail/payment/body.gotxt:9
|
||||||
|
msgid "Total: **%s**"
|
||||||
|
msgstr "Totale : **%s**"
|
||||||
|
|
||||||
#: web/templates/public/payment/success.gohtml:6
|
#: web/templates/public/payment/success.gohtml:6
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Payment Successful"
|
msgid "Payment Successful"
|
||||||
|
@ -1979,11 +2039,16 @@ msgstr "Statut"
|
||||||
msgid "No booking found."
|
msgid "No booking found."
|
||||||
msgstr "Aucune réservation trouvée."
|
msgstr "Aucune réservation trouvée."
|
||||||
|
|
||||||
#: pkg/payment/public.go:97
|
#: pkg/payment/public.go:107
|
||||||
msgctxt "order product name"
|
msgctxt "order product name"
|
||||||
msgid "Campsite Booking"
|
msgid "Campsite Booking"
|
||||||
msgstr "Réservation camping"
|
msgstr "Réservation camping"
|
||||||
|
|
||||||
|
#: pkg/payment/public.go:344
|
||||||
|
msgctxt "subject"
|
||||||
|
msgid "Booking payment successfully received"
|
||||||
|
msgstr "Paiement de réservation reçu avec succès"
|
||||||
|
|
||||||
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
|
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
|
||||||
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
||||||
#: pkg/campsite/feature.go:269 pkg/season/admin.go:412
|
#: pkg/campsite/feature.go:269 pkg/season/admin.go:412
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="{{ .CurrentLocale }}">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<title>{{( pgettext "Booking Payment Notification" "title" )}}</title>
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Helvetica, sans-serif; -webkit-font-smoothing: antialiased; font-size: 16px; line-height: 1.3; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; background-color: #f4f5f6; margin: 0; padding: 0;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f4f5f6; width: 100%;"
|
||||||
|
width="100%" bgcolor="#f4f5f6">
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top;" valign="top"> </td>
|
||||||
|
<td class="container"
|
||||||
|
style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; max-width: 600px; padding: 0; padding-top: 24px; width: 600px; margin: 0 auto;"
|
||||||
|
width="600" valign="top">
|
||||||
|
<div class="content"
|
||||||
|
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px; padding: 0;">
|
||||||
|
|
||||||
|
<span class="preheader"
|
||||||
|
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border: 1px solid #eaebed; border-radius: 16px; width: 100%;"
|
||||||
|
width="100%">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper"
|
||||||
|
style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; box-sizing: border-box; padding: 24px;"
|
||||||
|
valign="top">
|
||||||
|
<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">
|
||||||
|
{{ printf (gettext "Hi %s,") .CustomerFullName }}</p>
|
||||||
|
<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">
|
||||||
|
{{( gettext "We have successfully received the payment for the booking with the following details:" )}}</p>
|
||||||
|
<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">
|
||||||
|
{{ (printf ( gettext "Payment reference: <strong>%s</strong>") .PaymentReference) | raw }}
|
||||||
|
<br>
|
||||||
|
{{ (printf ( gettext "Accommodation: <strong>%s</strong>") .AccommodationName) | raw }}
|
||||||
|
<br>
|
||||||
|
{{ (printf ( gettext "Arrival Date: <strong>%s</strong>") .ArrivalDate) | raw }}
|
||||||
|
<br>
|
||||||
|
{{ (printf ( gettext "Departure Date: <strong>%s</strong>") .DepartureDate) | raw }}
|
||||||
|
<br>
|
||||||
|
{{ (printf ( gettext "Total: <strong>%s</strong>") .Total) | raw }}
|
||||||
|
</p>
|
||||||
|
<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">
|
||||||
|
{{( gettext "Thank you for your booking, and see you soon!" )}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{ with .CompanyAddress -}}
|
||||||
|
<div class="footer" style="clear: both; padding-top: 24px; text-align: center; width: 100%;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"
|
||||||
|
width="100%">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block"
|
||||||
|
style="font-family: Helvetica, sans-serif; vertical-align: top; color: #9a9ea6; font-size: 16px; text-align: center;"
|
||||||
|
valign="top" align="center">
|
||||||
|
<span class="apple-link"
|
||||||
|
style="color: #9a9ea6; font-size: 16px; text-align: center;">{{ .TradeName }}, {{ .Address }}, {{ .PostalCode}} · {{ .City }} · {{ .Province }} · {{ .Country }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top;" valign="top"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,18 @@
|
||||||
|
{{ printf (gettext "Hi %s,") .CustomerFullName }}
|
||||||
|
|
||||||
|
{{( gettext "We have successfully received the payment for the booking with the following details:" )}}</p>
|
||||||
|
|
||||||
|
{{ printf ( gettext "Payment reference: **%s**") .PaymentReference }}
|
||||||
|
{{ printf ( gettext "Accommodation: **%s**") .AccommodationName }}
|
||||||
|
{{ printf ( gettext "Arrival Date: **%s**") .ArrivalDate }}
|
||||||
|
{{ printf ( gettext "Departure Date: **%s**") .DepartureDate }}
|
||||||
|
{{ printf ( gettext "Total: **%s**") .Total }}
|
||||||
|
|
||||||
|
{{( gettext "Thank you for your booking, and see you soon!" )}}
|
||||||
|
|
||||||
|
{{ with .CompanyAddress -}}
|
||||||
|
{{ .TradeName }},
|
||||||
|
{{ .Address }},
|
||||||
|
{{ .PostalCode}} · {{ .City }} · {{ .Province }}
|
||||||
|
{{ .Country }}
|
||||||
|
{{- end }}
|
Loading…
Reference in New Issue