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)
|
||||
DEFAULT_DOMAIN = camper
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
ht "html/template"
|
||||
"net/http"
|
||||
tt "text/template"
|
||||
"time"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
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/template"
|
||||
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
StatusDraft = "draft"
|
||||
StatusPending = "pending"
|
||||
StatusFailed = "failed"
|
||||
StatusCompleted = "completed"
|
||||
StatusRefunded = "refunded"
|
||||
)
|
||||
|
||||
type PublicHandler struct {
|
||||
|
@ -249,6 +249,7 @@ func handleNotification(w http.ResponseWriter, r *http.Request, user *auth.User,
|
|||
}
|
||||
switch status {
|
||||
case StatusCompleted:
|
||||
_ = sendEmail(r.Context(), conn, payment, company, user.Locale) /* shrug */
|
||||
default:
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
@ -259,3 +260,109 @@ func handleNotification(w http.ResponseWriter, r *http.Request, user *auth.User,
|
|||
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)
|
||||
},
|
||||
"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 {
|
||||
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)
|
||||
f, err := strconv.ParseFloat(price, 64)
|
||||
if err != nil {
|
||||
|
|
69
po/ca.po
69
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: camper\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"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -59,6 +59,66 @@ msgctxt "tooltip"
|
|||
msgid "Zone 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
|
||||
msgctxt "title"
|
||||
msgid "Payment Successful"
|
||||
|
@ -1979,11 +2039,16 @@ msgstr "Estat"
|
|||
msgid "No booking found."
|
||||
msgstr "No s’ha trobat cap reserva."
|
||||
|
||||
#: pkg/payment/public.go:97
|
||||
#: pkg/payment/public.go:107
|
||||
msgctxt "order product name"
|
||||
msgid "Campsite Booking"
|
||||
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/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
||||
#: pkg/campsite/feature.go:269 pkg/season/admin.go:412
|
||||
|
|
69
po/es.po
69
po/es.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: camper\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"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -59,6 +59,66 @@ msgctxt "tooltip"
|
|||
msgid "Zone 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
|
||||
msgctxt "title"
|
||||
msgid "Payment Successful"
|
||||
|
@ -1979,11 +2039,16 @@ msgstr "Estado"
|
|||
msgid "No booking found."
|
||||
msgstr "No se ha encontrado ninguna reserva."
|
||||
|
||||
#: pkg/payment/public.go:97
|
||||
#: pkg/payment/public.go:107
|
||||
msgctxt "order product name"
|
||||
msgid "Campsite Booking"
|
||||
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/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
||||
#: pkg/campsite/feature.go:269 pkg/season/admin.go:412
|
||||
|
|
69
po/fr.po
69
po/fr.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: camper\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"
|
||||
"Last-Translator: Oriol Carbonell <info@oriolcarbonell.cat>\n"
|
||||
"Language-Team: French <traduc@traduc.org>\n"
|
||||
|
@ -59,6 +59,66 @@ msgctxt "tooltip"
|
|||
msgid "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
|
||||
msgctxt "title"
|
||||
msgid "Payment Successful"
|
||||
|
@ -1979,11 +2039,16 @@ msgstr "Statut"
|
|||
msgid "No booking found."
|
||||
msgstr "Aucune réservation trouvée."
|
||||
|
||||
#: pkg/payment/public.go:97
|
||||
#: pkg/payment/public.go:107
|
||||
msgctxt "order product name"
|
||||
msgid "Campsite Booking"
|
||||
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/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
|
||||
#: 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