wg-portal/internal/adapters/mailer.go
h44z 8b820a5adf
V2 alpha - initial version (#172)
Initial alpha codebase for version 2 of WireGuard Portal.
This version is considered unstable and incomplete (for example, no public REST API)! 
Use with care!


Fixes/Implements the following issues:
 - OAuth support #154, #1 
 - New Web UI with internationalisation support #98, #107, #89, #62
 - Postgres Support #49 
 - Improved Email handling #47, #119 
 - DNS Search Domain support #46 
 - Bugfixes #94, #48 

---------

Co-authored-by: Fabian Wechselberger <wechselbergerf@hotmail.com>
2023-08-04 13:34:18 +02:00

139 lines
3.7 KiB
Go

package adapters
import (
"context"
"crypto/tls"
"errors"
"fmt"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
mail "github.com/xhit/go-simple-mail/v2"
"io"
"time"
)
type MailRepo struct {
cfg *config.MailConfig
}
func NewSmtpMailRepo(cfg config.MailConfig) MailRepo {
return MailRepo{cfg: &cfg}
}
// Send sends a mail.
func (r MailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error {
if options == nil {
options = &domain.MailOptions{}
}
r.setDefaultOptions(r.cfg.From, options)
if len(to) == 0 {
return errors.New("missing email recipient")
}
uniqueTo := internal.UniqueStringSlice(to)
email := mail.NewMSG()
email.SetFrom(r.cfg.From).
AddTo(uniqueTo...).
SetReplyTo(options.ReplyTo).
SetSubject(subject).
SetBody(mail.TextPlain, body)
if len(options.Cc) > 0 {
// the underlying mail library does not allow the same address to appear in TO and CC... so filter entries that are already included
// in the TO addresses
cc := RemoveDuplicates(internal.UniqueStringSlice(options.Cc), uniqueTo)
email.AddCc(cc...)
}
if len(options.Bcc) > 0 {
// the underlying mail library does not allow the same address to appear in TO or CC and BCC... so filter entries that are already
// included in the TO and CC addresses
bcc := RemoveDuplicates(internal.UniqueStringSlice(options.Bcc), uniqueTo)
bcc = RemoveDuplicates(bcc, options.Cc)
email.AddCc(internal.UniqueStringSlice(options.Bcc)...)
}
if options.HtmlBody != "" {
email.AddAlternative(mail.TextHTML, options.HtmlBody)
}
for _, attachment := range options.Attachments {
attachmentData, err := io.ReadAll(attachment.Data)
if err != nil {
return fmt.Errorf("failed to read attachment data for %s: %w", attachment.Name, err)
}
if attachment.Embedded {
email.AddInlineData(attachmentData, attachment.Name, attachment.ContentType)
} else {
email.AddAttachmentData(attachmentData, attachment.Name, attachment.ContentType)
}
}
// Call Send and pass the client
srv := r.getMailServer()
client, err := srv.Connect()
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
err = email.Send(client)
if err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}
func (r MailRepo) setDefaultOptions(sender string, options *domain.MailOptions) {
if options.ReplyTo == "" {
options.ReplyTo = sender
}
}
func (r MailRepo) getMailServer() *mail.SMTPServer {
srv := mail.NewSMTPClient()
srv.ConnectTimeout = 30 * time.Second
srv.SendTimeout = 30 * time.Second
srv.Host = r.cfg.Host
srv.Port = r.cfg.Port
srv.Username = r.cfg.Username
srv.Password = r.cfg.Password
switch r.cfg.Encryption {
case config.MailEncryptionTLS:
srv.Encryption = mail.EncryptionSSLTLS
case config.MailEncryptionStartTLS:
srv.Encryption = mail.EncryptionSTARTTLS
default: // MailEncryptionNone
srv.Encryption = mail.EncryptionNone
}
srv.TLSConfig = &tls.Config{ServerName: srv.Host, InsecureSkipVerify: !r.cfg.CertValidation}
switch r.cfg.AuthType {
case config.MailAuthPlain:
srv.Authentication = mail.AuthPlain
case config.MailAuthLogin:
srv.Authentication = mail.AuthLogin
case config.MailAuthCramMD5:
srv.Authentication = mail.AuthCRAMMD5
}
return srv
}
// RemoveDuplicates removes addresses from the given string slice which are contained in the remove slice.
func RemoveDuplicates(slice []string, remove []string) []string {
uniqueSlice := make([]string, 0, len(slice))
for _, i := range remove {
for _, j := range slice {
if i != j {
uniqueSlice = append(uniqueSlice, j)
}
}
}
return uniqueSlice
}