Ajout d'une newsletter basique
La newsletter effectue une collecte des évènements sur une période de temps donné et envoi un récapitulatif à l'ensemble des utilisateurs de Daddy. Actuellement, sont collectés et présentés: - Les créations de groupes de travail - Les créations de dossiers d'aide à la décision - Les dossiers dont le statut à été modifié et prêt à voté
This commit is contained in:
52
internal/mail/mailer.go
Normal file
52
internal/mail/mailer.go
Normal file
@ -0,0 +1,52 @@
|
||||
package mail
|
||||
|
||||
const (
|
||||
ContentTypeHTML = "text/html"
|
||||
ContentTypeText = "text/plain"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
InsecureSkipVerify bool
|
||||
UseStartTLS bool
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
|
||||
type Mailer struct {
|
||||
opt *Option
|
||||
}
|
||||
|
||||
func WithTLS(useStartTLS, insecureSkipVerify bool) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.UseStartTLS = useStartTLS
|
||||
opt.InsecureSkipVerify = insecureSkipVerify
|
||||
}
|
||||
}
|
||||
|
||||
func WithServer(host string, port int) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Host = host
|
||||
opt.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithCredentials(user, password string) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.User = user
|
||||
opt.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
func NewMailer(funcs ...OptionFunc) *Mailer {
|
||||
opt := &Option{}
|
||||
|
||||
for _, fn := range funcs {
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
return &Mailer{opt}
|
||||
}
|
11
internal/mail/provider.go
Normal file
11
internal/mail/provider.go
Normal file
@ -0,0 +1,11 @@
|
||||
package mail
|
||||
|
||||
import "gitlab.com/wpetit/goweb/service"
|
||||
|
||||
func ServiceProvider(opts ...OptionFunc) service.Provider {
|
||||
mailer := NewMailer(opts...)
|
||||
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
return mailer, nil
|
||||
}
|
||||
}
|
207
internal/mail/send.go
Normal file
207
internal/mail/send.go
Normal file
@ -0,0 +1,207 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnexpectedEmailAddressFormat = errors.New("unexpected email address format")
|
||||
)
|
||||
|
||||
type SendFunc func(*SendOption)
|
||||
|
||||
type SendOption struct {
|
||||
Charset string
|
||||
AddressHeaders []AddressHeader
|
||||
Headers []Header
|
||||
Body Body
|
||||
AlternativeBodies []Body
|
||||
}
|
||||
|
||||
type AddressHeader struct {
|
||||
Field string
|
||||
Address string
|
||||
Name string
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Field string
|
||||
Values []string
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Type string
|
||||
Content string
|
||||
PartSetting gomail.PartSetting
|
||||
}
|
||||
|
||||
func WithCharset(charset string) func(*SendOption) {
|
||||
return func(opt *SendOption) {
|
||||
opt.Charset = charset
|
||||
}
|
||||
}
|
||||
|
||||
func WithSender(address string, name string) func(*SendOption) {
|
||||
return WithAddressHeader("From", address, name)
|
||||
}
|
||||
|
||||
func WithSubject(subject string) func(*SendOption) {
|
||||
return WithHeader("Subject", subject)
|
||||
}
|
||||
|
||||
func WithAddressHeader(field, address, name string) func(*SendOption) {
|
||||
return func(opt *SendOption) {
|
||||
opt.AddressHeaders = append(opt.AddressHeaders, AddressHeader{field, address, name})
|
||||
}
|
||||
}
|
||||
|
||||
func WithHeader(field string, values ...string) func(*SendOption) {
|
||||
return func(opt *SendOption) {
|
||||
opt.Headers = append(opt.Headers, Header{field, values})
|
||||
}
|
||||
}
|
||||
|
||||
func WithRecipients(addresses ...string) func(*SendOption) {
|
||||
return WithHeader("To", addresses...)
|
||||
}
|
||||
|
||||
func WithCopies(addresses ...string) func(*SendOption) {
|
||||
return WithHeader("Cc", addresses...)
|
||||
}
|
||||
|
||||
func WithInvisibleCopies(addresses ...string) func(*SendOption) {
|
||||
return WithHeader("Cci", addresses...)
|
||||
}
|
||||
|
||||
func WithBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
||||
return func(opt *SendOption) {
|
||||
if setting == nil {
|
||||
setting = gomail.SetPartEncoding(gomail.Unencoded)
|
||||
}
|
||||
opt.Body = Body{contentType, content, setting}
|
||||
}
|
||||
}
|
||||
|
||||
func WithAlternativeBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
||||
return func(opt *SendOption) {
|
||||
if setting == nil {
|
||||
setting = gomail.SetPartEncoding(gomail.Unencoded)
|
||||
}
|
||||
opt.AlternativeBodies = append(opt.AlternativeBodies, Body{contentType, content, setting})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mailer) Send(funcs ...SendFunc) error {
|
||||
opt := &SendOption{
|
||||
Charset: "UTF-8",
|
||||
Body: Body{
|
||||
Type: "text/plain",
|
||||
Content: "",
|
||||
PartSetting: gomail.SetPartEncoding(gomail.Unencoded),
|
||||
},
|
||||
AddressHeaders: make([]AddressHeader, 0),
|
||||
Headers: make([]Header, 0),
|
||||
AlternativeBodies: make([]Body, 0),
|
||||
}
|
||||
|
||||
for _, f := range funcs {
|
||||
f(opt)
|
||||
}
|
||||
|
||||
conn, err := m.openConnection()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not open connection")
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
message := gomail.NewMessage(gomail.SetCharset(opt.Charset))
|
||||
|
||||
for _, h := range opt.AddressHeaders {
|
||||
message.SetAddressHeader(h.Field, h.Address, h.Name)
|
||||
}
|
||||
|
||||
for _, h := range opt.Headers {
|
||||
message.SetHeader(h.Field, h.Values...)
|
||||
}
|
||||
|
||||
froms := message.GetHeader("From")
|
||||
|
||||
var sendDomain string
|
||||
|
||||
if len(froms) > 0 {
|
||||
sendDomain, err = extractEmailDomain(froms[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
messageID := generateMessageID(sendDomain)
|
||||
message.SetHeader("Message-Id", messageID)
|
||||
|
||||
message.SetBody(opt.Body.Type, opt.Body.Content, opt.Body.PartSetting)
|
||||
|
||||
for _, b := range opt.AlternativeBodies {
|
||||
message.AddAlternative(b.Type, b.Content, b.PartSetting)
|
||||
}
|
||||
|
||||
if err := gomail.Send(conn, message); err != nil {
|
||||
return errors.Wrap(err, "could not send message")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mailer) openConnection() (gomail.SendCloser, error) {
|
||||
dialer := gomail.NewDialer(
|
||||
m.opt.Host,
|
||||
m.opt.Port,
|
||||
m.opt.User,
|
||||
m.opt.Password,
|
||||
)
|
||||
|
||||
if m.opt.InsecureSkipVerify {
|
||||
dialer.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not dial smtp server")
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func extractEmailDomain(email string) (string, error) {
|
||||
address, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "could not parse email address '%s'", email)
|
||||
}
|
||||
|
||||
addressParts := strings.SplitN(address.Address, "@", 2)
|
||||
if len(addressParts) != 2 { // nolint: gomnd
|
||||
return "", errors.WithStack(ErrUnexpectedEmailAddressFormat)
|
||||
}
|
||||
|
||||
domain := addressParts[1]
|
||||
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
func generateMessageID(domain string) string {
|
||||
// Based on https://www.jwz.org/doc/mid.html
|
||||
timestamp := strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||
random := strconv.FormatInt(rand.Int63(), 36)
|
||||
return fmt.Sprintf("<%s.%s@%s>", timestamp, random, domain)
|
||||
}
|
33
internal/mail/service.go
Normal file
33
internal/mail/service.go
Normal file
@ -0,0 +1,33 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "mail"
|
||||
|
||||
// From retrieves the mail service in the given container
|
||||
func From(container *service.Container) (*Mailer, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*Mailer)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the mail service in the given container or panic otherwise
|
||||
func Must(container *service.Container) *Mailer {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
Reference in New Issue
Block a user