mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-16 07:16:12 +00:00
fix: support LOGIN authentication method for SMTP (#292)
This commit is contained in:
@@ -6,6 +6,8 @@ require (
|
|||||||
github.com/caarlos0/env/v11 v11.3.1
|
github.com/caarlos0/env/v11 v11.3.1
|
||||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||||
|
github.com/emersion/go-smtp v0.21.3
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0
|
github.com/fxamacker/cbor/v2 v2.7.0
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-co-op/gocron/v2 v2.15.0
|
github.com/go-co-op/gocron/v2 v2.15.0
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
|||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
|
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
|
||||||
|
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
|
|||||||
@@ -3,27 +3,23 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
htemplate "html/template"
|
"github.com/emersion/go-sasl"
|
||||||
"mime/multipart"
|
"github.com/emersion/go-smtp"
|
||||||
"mime/quotedprintable"
|
|
||||||
"net"
|
|
||||||
"net/smtp"
|
|
||||||
"net/textproto"
|
|
||||||
"os"
|
|
||||||
ttemplate "text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
htemplate "html/template"
|
||||||
|
"mime/multipart"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"net/textproto"
|
||||||
|
"os"
|
||||||
|
ttemplate "text/template"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var netDialer = &net.Dialer{
|
|
||||||
Timeout: 3 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmailService struct {
|
type EmailService struct {
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
@@ -114,18 +110,14 @@ func (srv *EmailService) getSmtpClient() (client *smtp.Client, err error) {
|
|||||||
ServerName: srv.appConfigService.DbConfig.SmtpHost.Value,
|
ServerName: srv.appConfigService.DbConfig.SmtpHost.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to the SMTP server
|
|
||||||
// Connect to the SMTP server based on TLS setting
|
// Connect to the SMTP server based on TLS setting
|
||||||
switch srv.appConfigService.DbConfig.SmtpTls.Value {
|
switch srv.appConfigService.DbConfig.SmtpTls.Value {
|
||||||
case "none":
|
case "none":
|
||||||
client, err = srv.connectToSmtpServer(smtpAddress)
|
client, err = smtp.Dial(smtpAddress)
|
||||||
case "tls":
|
case "tls":
|
||||||
client, err = srv.connectToSmtpServerUsingImplicitTLS(
|
client, err = smtp.DialTLS(smtpAddress, tlsConfig)
|
||||||
smtpAddress,
|
|
||||||
tlsConfig,
|
|
||||||
)
|
|
||||||
case "starttls":
|
case "starttls":
|
||||||
client, err = srv.connectToSmtpServerUsingStartTLS(
|
client, err = smtp.DialStartTLS(
|
||||||
smtpAddress,
|
smtpAddress,
|
||||||
tlsConfig,
|
tlsConfig,
|
||||||
)
|
)
|
||||||
@@ -136,87 +128,39 @@ func (srv *EmailService) getSmtpClient() (client *smtp.Client, err error) {
|
|||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.CommandTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
// Send the HELO command
|
||||||
|
if err := srv.sendHelloCommand(client); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send HELO command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Set up the authentication if user or password are set
|
// Set up the authentication if user or password are set
|
||||||
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
||||||
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
||||||
|
|
||||||
if smtpUser != "" || smtpPassword != "" {
|
if smtpUser != "" || smtpPassword != "" {
|
||||||
auth := smtp.PlainAuth("",
|
// Authenticate with plain auth
|
||||||
srv.appConfigService.DbConfig.SmtpUser.Value,
|
auth := sasl.NewPlainClient("", smtpUser, smtpPassword)
|
||||||
srv.appConfigService.DbConfig.SmtpPassword.Value,
|
|
||||||
srv.appConfigService.DbConfig.SmtpHost.Value,
|
|
||||||
)
|
|
||||||
if err := client.Auth(auth); err != nil {
|
if err := client.Auth(auth); err != nil {
|
||||||
return nil, fmt.Errorf("failed to authenticate SMTP client: %w", err)
|
// If the server does not support plain auth, try login auth
|
||||||
|
var smtpErr *smtp.SMTPError
|
||||||
|
ok := errors.As(err, &smtpErr)
|
||||||
|
if ok && smtpErr.Code == smtp.ErrAuthUnknownMechanism.Code {
|
||||||
|
auth = sasl.NewLoginClient(smtpUser, smtpPassword)
|
||||||
|
err = client.Auth(auth)
|
||||||
|
}
|
||||||
|
// Both plain and login auth failed
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to authenticate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, err
|
return client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServer(serverAddr string) (*smtp.Client, error) {
|
|
||||||
conn, err := netDialer.Dial("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
client, err := smtp.NewClient(conn, srv.appConfigService.DbConfig.SmtpHost.Value)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.sendHelloCommand(client); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
|
||||||
tlsDialer := &tls.Dialer{
|
|
||||||
NetDialer: netDialer,
|
|
||||||
Config: tlsConfig,
|
|
||||||
}
|
|
||||||
conn, err := tlsDialer.Dial("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := smtp.NewClient(conn, srv.appConfigService.DbConfig.SmtpHost.Value)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.sendHelloCommand(client); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingStartTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
|
||||||
conn, err := netDialer.Dial("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := smtp.NewClient(conn, srv.appConfigService.DbConfig.SmtpHost.Value)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.sendHelloCommand(client); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.StartTLS(tlsConfig); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to start TLS: %w", err)
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *EmailService) sendHelloCommand(client *smtp.Client) error {
|
func (srv *EmailService) sendHelloCommand(client *smtp.Client) error {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -228,23 +172,33 @@ func (srv *EmailService) sendHelloCommand(client *smtp.Client) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) sendEmailContent(client *smtp.Client, toEmail email.Address, c *email.Composer) error {
|
func (srv *EmailService) sendEmailContent(client *smtp.Client, toEmail email.Address, c *email.Composer) error {
|
||||||
if err := client.Mail(srv.appConfigService.DbConfig.SmtpFrom.Value); err != nil {
|
// Set the sender
|
||||||
|
if err := client.Mail(srv.appConfigService.DbConfig.SmtpFrom.Value, nil); err != nil {
|
||||||
return fmt.Errorf("failed to set sender: %w", err)
|
return fmt.Errorf("failed to set sender: %w", err)
|
||||||
}
|
}
|
||||||
if err := client.Rcpt(toEmail.Email); err != nil {
|
|
||||||
|
// Set the recipient
|
||||||
|
if err := client.Rcpt(toEmail.Email, nil); err != nil {
|
||||||
return fmt.Errorf("failed to set recipient: %w", err)
|
return fmt.Errorf("failed to set recipient: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a writer to write the email data
|
||||||
w, err := client.Data()
|
w, err := client.Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to start data: %w", err)
|
return fmt.Errorf("failed to start data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write the email content
|
||||||
_, err = w.Write([]byte(c.String()))
|
_, err = w.Write([]byte(c.String()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write email data: %w", err)
|
return fmt.Errorf("failed to write email data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the writer
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
return fmt.Errorf("failed to close data writer: %w", err)
|
return fmt.Errorf("failed to close data writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user