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:
jordi fita mas 2024-02-13 05:20:35 +01:00
parent 1179ba9c9a
commit 4a7b0112ef
15 changed files with 1330 additions and 13 deletions

View File

@ -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

View File

@ -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
}

61
pkg/mail/encoding.go Normal file
View File

@ -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)
}

54
pkg/mail/header.go Normal file
View File

@ -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"
)

362
pkg/mail/msg.go Normal file
View File

@ -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
}

282
pkg/mail/msgwrite.go Normal file
View File

@ -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
}
}

18
pkg/mail/part.go Normal file
View File

@ -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)
}

75
pkg/mail/random.go Normal file
View File

@ -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
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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 darribada: <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 darribada: **%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 sha 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

View File

@ -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

View File

@ -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 darrivé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 darrivé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

View File

@ -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">&nbsp;</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">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -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 }}