diff --git a/backend/go.mod b/backend/go.mod index 940fbbe..2658374 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -6,6 +6,8 @@ require ( github.com/caarlos0/env/v11 v11.3.1 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec 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/gin-gonic/gin v1.10.0 github.com/go-co-op/gocron/v2 v2.15.0 diff --git a/backend/go.sum b/backend/go.sum index a1a312b..0a0889a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= diff --git a/backend/internal/service/email_service.go b/backend/internal/service/email_service.go index 33f3e6b..5a6fc4f 100644 --- a/backend/internal/service/email_service.go +++ b/backend/internal/service/email_service.go @@ -3,27 +3,23 @@ package service import ( "bytes" "crypto/tls" + "errors" "fmt" - htemplate "html/template" - "mime/multipart" - "mime/quotedprintable" - "net" - "net/smtp" - "net/textproto" - "os" - ttemplate "text/template" - "time" - + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" "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/utils/email" "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 { appConfigService *AppConfigService db *gorm.DB @@ -114,18 +110,14 @@ func (srv *EmailService) getSmtpClient() (client *smtp.Client, err error) { ServerName: srv.appConfigService.DbConfig.SmtpHost.Value, } - // Connect to the SMTP server // Connect to the SMTP server based on TLS setting switch srv.appConfigService.DbConfig.SmtpTls.Value { case "none": - client, err = srv.connectToSmtpServer(smtpAddress) + client, err = smtp.Dial(smtpAddress) case "tls": - client, err = srv.connectToSmtpServerUsingImplicitTLS( - smtpAddress, - tlsConfig, - ) + client, err = smtp.DialTLS(smtpAddress, tlsConfig) case "starttls": - client, err = srv.connectToSmtpServerUsingStartTLS( + client, err = smtp.DialStartTLS( smtpAddress, 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) } + 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 smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value if smtpUser != "" || smtpPassword != "" { - auth := smtp.PlainAuth("", - srv.appConfigService.DbConfig.SmtpUser.Value, - srv.appConfigService.DbConfig.SmtpPassword.Value, - srv.appConfigService.DbConfig.SmtpHost.Value, - ) + // Authenticate with plain auth + auth := sasl.NewPlainClient("", smtpUser, smtpPassword) 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 } -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 { hostname, err := os.Hostname() 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 { - 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) } - 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) } + + // Get a writer to write the email data w, err := client.Data() if err != nil { return fmt.Errorf("failed to start data: %w", err) } + + // Write the email content _, err = w.Write([]byte(c.String())) if err != nil { return fmt.Errorf("failed to write email data: %w", err) } + + // Close the writer if err := w.Close(); err != nil { return fmt.Errorf("failed to close data writer: %w", err) } + return nil }