mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:35:03 +01:00 
			
		
		
		
	Split mail sender sub package from mailer service package (#32618)
Move all mail sender related codes into a sub package of services/mailer. Just move, no code change. Then we just have dependencies on go-mail package in the new sub package. We can use other package to replace it because it's unmaintainable. ref #18664
This commit is contained in:
		
							parent
							
								
									fd3aa5bedb
								
							
						
					
					
						commit
						79d593a9be
					
				| @ -17,6 +17,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/mailer" | ||||
| 	sender_service "code.gitea.io/gitea/services/mailer/sender" | ||||
| ) | ||||
| 
 | ||||
| // SendEmail pushes messages to mail queue | ||||
| @ -81,7 +82,7 @@ func SendEmail(ctx *context.PrivateContext) { | ||||
| 
 | ||||
| func sendEmail(ctx *context.PrivateContext, subject, message string, to []string) { | ||||
| 	for _, email := range to { | ||||
| 		msg := mailer.NewMessage(email, subject, message) | ||||
| 		msg := sender_service.NewMessage(email, subject, message) | ||||
| 		mailer.SendAsync(msg) | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -29,9 +29,8 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 	incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload" | ||||
| 	sender_service "code.gitea.io/gitea/services/mailer/sender" | ||||
| 	"code.gitea.io/gitea/services/mailer/token" | ||||
| 
 | ||||
| 	"gopkg.in/gomail.v2" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| @ -60,7 +59,7 @@ func SendTestMail(email string) error { | ||||
| 		// No mail service configured | ||||
| 		return nil | ||||
| 	} | ||||
| 	return gomail.Send(Sender, NewMessage(email, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) | ||||
| 	return sender_service.Send(sender, sender_service.NewMessage(email, "Gitea Test Email!", "Gitea Test Email!")) | ||||
| } | ||||
| 
 | ||||
| // sendUserMail sends a mail to the user | ||||
| @ -82,7 +81,7 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(u.EmailTo(), subject, content.String()) | ||||
| 	msg := sender_service.NewMessage(u.EmailTo(), subject, content.String()) | ||||
| 	msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info) | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
| @ -130,7 +129,7 @@ func SendActivateEmailMail(u *user_model.User, email string) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String()) | ||||
| 	msg := sender_service.NewMessage(email, locale.TrString("mail.activate_email"), content.String()) | ||||
| 	msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID) | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
| @ -158,7 +157,7 @@ func SendRegisterNotifyMail(u *user_model.User) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(u.EmailTo(), locale.TrString("mail.register_notify", setting.AppName), content.String()) | ||||
| 	msg := sender_service.NewMessage(u.EmailTo(), locale.TrString("mail.register_notify", setting.AppName), content.String()) | ||||
| 	msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID) | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
| @ -189,13 +188,13 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(u.EmailTo(), subject, content.String()) | ||||
| 	msg := sender_service.NewMessage(u.EmailTo(), subject, content.String()) | ||||
| 	msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID) | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
| } | ||||
| 
 | ||||
| func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipients []*user_model.User, fromMention bool, info string) ([]*Message, error) { | ||||
| func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipients []*user_model.User, fromMention bool, info string) ([]*sender_service.Message, error) { | ||||
| 	var ( | ||||
| 		subject string | ||||
| 		link    string | ||||
| @ -304,9 +303,9 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	msgs := make([]*Message, 0, len(recipients)) | ||||
| 	msgs := make([]*sender_service.Message, 0, len(recipients)) | ||||
| 	for _, recipient := range recipients { | ||||
| 		msg := NewMessageFrom( | ||||
| 		msg := sender_service.NewMessageFrom( | ||||
| 			recipient.Email, | ||||
| 			fromDisplayName(ctx.Doer), | ||||
| 			setting.MailService.FromEmail, | ||||
|  | ||||
| @ -15,6 +15,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 	sender_service "code.gitea.io/gitea/services/mailer/sender" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| @ -80,11 +81,11 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	msgs := make([]*Message, 0, len(tos)) | ||||
| 	msgs := make([]*sender_service.Message, 0, len(tos)) | ||||
| 	publisherName := fromDisplayName(rel.Publisher) | ||||
| 	msgID := generateMessageIDForRelease(rel) | ||||
| 	for _, to := range tos { | ||||
| 		msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String()) | ||||
| 		msg := sender_service.NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String()) | ||||
| 		msg.Info = subject | ||||
| 		msg.SetHeader("Message-ID", msgID) | ||||
| 		msgs = append(msgs, msg) | ||||
|  | ||||
| @ -13,6 +13,7 @@ import ( | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 	sender_service "code.gitea.io/gitea/services/mailer/sender" | ||||
| ) | ||||
| 
 | ||||
| // SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created | ||||
| @ -79,7 +80,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U | ||||
| 	} | ||||
| 
 | ||||
| 	for _, to := range emailTos { | ||||
| 		msg := NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String()) | ||||
| 		msg := sender_service.NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String()) | ||||
| 		msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID) | ||||
| 
 | ||||
| 		SendAsync(msg) | ||||
|  | ||||
| @ -15,6 +15,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 	sender_service "code.gitea.io/gitea/services/mailer/sender" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| @ -67,7 +68,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(invite.Email, subject, mailBody.String()) | ||||
| 	msg := sender_service.NewMessage(invite.Email, subject, mailBody.String()) | ||||
| 	msg.Info = subject | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
|  | ||||
| @ -23,6 +23,7 @@ import ( | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	sender_service "code.gitea.io/gitea/services/mailer/sender" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| @ -167,7 +168,7 @@ func TestTemplateSelection(t *testing.T) { | ||||
| 	template.Must(bodyTemplates.New("pull/comment").Parse("pull/comment/body")) | ||||
| 	template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body")) | ||||
| 
 | ||||
| 	expect := func(t *testing.T, msg *Message, expSubject, expBody string) { | ||||
| 	expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) { | ||||
| 		subject := msg.ToMessage().GetHeader("Subject") | ||||
| 		msgbuf := new(bytes.Buffer) | ||||
| 		_, _ = msg.ToMessage().WriteTo(msgbuf) | ||||
| @ -252,7 +253,7 @@ func TestTemplateServices(t *testing.T) { | ||||
| 		"//Re: //") | ||||
| } | ||||
| 
 | ||||
| func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recipients []*user_model.User, fromMention bool, info string) *Message { | ||||
| func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recipients []*user_model.User, fromMention bool, info string) *sender_service.Message { | ||||
| 	msgs, err := composeIssueCommentMessages(ctx, "en-US", recipients, fromMention, info) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Len(t, msgs, 1) | ||||
|  | ||||
| @ -5,391 +5,21 @@ | ||||
| package mailer | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"hash/fnv" | ||||
| 	"io" | ||||
| 	"net" | ||||
| 	"net/smtp" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/process" | ||||
| 	"code.gitea.io/gitea/modules/queue" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/templates" | ||||
| 	sender_service "code.gitea.io/gitea/services/mailer/sender" | ||||
| 	notify_service "code.gitea.io/gitea/services/notify" | ||||
| 
 | ||||
| 	ntlmssp "github.com/Azure/go-ntlmssp" | ||||
| 	"github.com/jaytaylor/html2text" | ||||
| 	"gopkg.in/gomail.v2" | ||||
| ) | ||||
| 
 | ||||
| // Message mail body and log info | ||||
| type Message struct { | ||||
| 	Info            string // Message information for log purpose. | ||||
| 	FromAddress     string | ||||
| 	FromDisplayName string | ||||
| 	To              string // Use only one recipient to prevent leaking of addresses | ||||
| 	ReplyTo         string | ||||
| 	Subject         string | ||||
| 	Date            time.Time | ||||
| 	Body            string | ||||
| 	Headers         map[string][]string | ||||
| } | ||||
| var mailQueue *queue.WorkerPoolQueue[*sender_service.Message] | ||||
| 
 | ||||
| // ToMessage converts a Message to gomail.Message | ||||
| func (m *Message) ToMessage() *gomail.Message { | ||||
| 	msg := gomail.NewMessage() | ||||
| 	msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) | ||||
| 	msg.SetHeader("To", m.To) | ||||
| 	if m.ReplyTo != "" { | ||||
| 		msg.SetHeader("Reply-To", m.ReplyTo) | ||||
| 	} | ||||
| 	for header := range m.Headers { | ||||
| 		msg.SetHeader(header, m.Headers[header]...) | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.MailService.SubjectPrefix != "" { | ||||
| 		msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) | ||||
| 	} else { | ||||
| 		msg.SetHeader("Subject", m.Subject) | ||||
| 	} | ||||
| 	msg.SetDateHeader("Date", m.Date) | ||||
| 	msg.SetHeader("X-Auto-Response-Suppress", "All") | ||||
| 
 | ||||
| 	plainBody, err := html2text.FromString(m.Body) | ||||
| 	if err != nil || setting.MailService.SendAsPlainText { | ||||
| 		if strings.Contains(base.TruncateString(m.Body, 100), "<html>") { | ||||
| 			log.Warn("Mail contains HTML but configured to send as plain text.") | ||||
| 		} | ||||
| 		msg.SetBody("text/plain", plainBody) | ||||
| 	} else { | ||||
| 		msg.SetBody("text/plain", plainBody) | ||||
| 		msg.AddAlternative("text/html", m.Body) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(msg.GetHeader("Message-ID")) == 0 { | ||||
| 		msg.SetHeader("Message-ID", m.generateAutoMessageID()) | ||||
| 	} | ||||
| 
 | ||||
| 	for k, v := range setting.MailService.OverrideHeader { | ||||
| 		if len(msg.GetHeader(k)) != 0 { | ||||
| 			log.Debug("Mailer override header '%s' as per config", k) | ||||
| 		} | ||||
| 		msg.SetHeader(k, v...) | ||||
| 	} | ||||
| 
 | ||||
| 	return msg | ||||
| } | ||||
| 
 | ||||
| // SetHeader adds additional headers to a message | ||||
| func (m *Message) SetHeader(field string, value ...string) { | ||||
| 	m.Headers[field] = value | ||||
| } | ||||
| 
 | ||||
| func (m *Message) generateAutoMessageID() string { | ||||
| 	dateMs := m.Date.UnixNano() / 1e6 | ||||
| 	h := fnv.New64() | ||||
| 	if len(m.To) > 0 { | ||||
| 		_, _ = h.Write([]byte(m.To)) | ||||
| 	} | ||||
| 	_, _ = h.Write([]byte(m.Subject)) | ||||
| 	_, _ = h.Write([]byte(m.Body)) | ||||
| 	return fmt.Sprintf("<autogen-%d-%016x@%s>", dateMs, h.Sum64(), setting.Domain) | ||||
| } | ||||
| 
 | ||||
| // NewMessageFrom creates new mail message object with custom From header. | ||||
| func NewMessageFrom(to, fromDisplayName, fromAddress, subject, body string) *Message { | ||||
| 	log.Trace("NewMessageFrom (body):\n%s", body) | ||||
| 
 | ||||
| 	return &Message{ | ||||
| 		FromAddress:     fromAddress, | ||||
| 		FromDisplayName: fromDisplayName, | ||||
| 		To:              to, | ||||
| 		Subject:         subject, | ||||
| 		Date:            time.Now(), | ||||
| 		Body:            body, | ||||
| 		Headers:         map[string][]string{}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // NewMessage creates new mail message object with default From header. | ||||
| func NewMessage(to, subject, body string) *Message { | ||||
| 	return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body) | ||||
| } | ||||
| 
 | ||||
| type loginAuth struct { | ||||
| 	username, password string | ||||
| } | ||||
| 
 | ||||
| // LoginAuth SMTP AUTH LOGIN Auth Handler | ||||
| func LoginAuth(username, password string) smtp.Auth { | ||||
| 	return &loginAuth{username, password} | ||||
| } | ||||
| 
 | ||||
| // Start start SMTP login auth | ||||
| func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { | ||||
| 	return "LOGIN", []byte{}, nil | ||||
| } | ||||
| 
 | ||||
| // Next next step of SMTP login auth | ||||
| func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { | ||||
| 	if more { | ||||
| 		switch string(fromServer) { | ||||
| 		case "Username:": | ||||
| 			return []byte(a.username), nil | ||||
| 		case "Password:": | ||||
| 			return []byte(a.password), nil | ||||
| 		default: | ||||
| 			return nil, fmt.Errorf("unknown fromServer: %s", string(fromServer)) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| type ntlmAuth struct { | ||||
| 	username, password, domain string | ||||
| 	domainNeeded               bool | ||||
| } | ||||
| 
 | ||||
| // NtlmAuth SMTP AUTH NTLM Auth Handler | ||||
| func NtlmAuth(username, password string) smtp.Auth { | ||||
| 	user, domain, domainNeeded := ntlmssp.GetDomain(username) | ||||
| 	return &ntlmAuth{user, password, domain, domainNeeded} | ||||
| } | ||||
| 
 | ||||
| // Start starts SMTP NTLM Auth | ||||
| func (a *ntlmAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { | ||||
| 	negotiateMessage, err := ntlmssp.NewNegotiateMessage(a.domain, "") | ||||
| 	return "NTLM", negotiateMessage, err | ||||
| } | ||||
| 
 | ||||
| // Next next step of SMTP ntlm auth | ||||
| func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) { | ||||
| 	if more { | ||||
| 		if len(fromServer) == 0 { | ||||
| 			return nil, fmt.Errorf("ntlm ChallengeMessage is empty") | ||||
| 		} | ||||
| 		authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded) | ||||
| 		return authenticateMessage, err | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| // Sender SMTP mail sender | ||||
| type smtpSender struct{} | ||||
| 
 | ||||
| // Send send email | ||||
| func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error { | ||||
| 	opts := setting.MailService | ||||
| 
 | ||||
| 	var network string | ||||
| 	var address string | ||||
| 	if opts.Protocol == "smtp+unix" { | ||||
| 		network = "unix" | ||||
| 		address = opts.SMTPAddr | ||||
| 	} else { | ||||
| 		network = "tcp" | ||||
| 		address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort) | ||||
| 	} | ||||
| 
 | ||||
| 	conn, err := net.Dial(network, address) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to establish network connection to SMTP server: %w", err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	var tlsconfig *tls.Config | ||||
| 	if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" { | ||||
| 		tlsconfig = &tls.Config{ | ||||
| 			InsecureSkipVerify: opts.ForceTrustServerCert, | ||||
| 			ServerName:         opts.SMTPAddr, | ||||
| 		} | ||||
| 
 | ||||
| 		if opts.UseClientCert { | ||||
| 			cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("could not load SMTP client certificate: %w", err) | ||||
| 			} | ||||
| 			tlsconfig.Certificates = []tls.Certificate{cert} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.Protocol == "smtps" { | ||||
| 		conn = tls.Client(conn, tlsconfig) | ||||
| 	} | ||||
| 
 | ||||
| 	host := "localhost" | ||||
| 	if opts.Protocol == "smtp+unix" { | ||||
| 		host = opts.SMTPAddr | ||||
| 	} | ||||
| 	client, err := smtp.NewClient(conn, host) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("could not initiate SMTP session: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.EnableHelo { | ||||
| 		hostname := opts.HeloHostname | ||||
| 		if len(hostname) == 0 { | ||||
| 			hostname, err = os.Hostname() | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("could not retrieve system hostname: %w", err) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if err = client.Hello(hostname); err != nil { | ||||
| 			return fmt.Errorf("failed to issue HELO command: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.Protocol == "smtp+starttls" { | ||||
| 		hasStartTLS, _ := client.Extension("STARTTLS") | ||||
| 		if hasStartTLS { | ||||
| 			if err = client.StartTLS(tlsconfig); err != nil { | ||||
| 				return fmt.Errorf("failed to start TLS connection: %w", err) | ||||
| 			} | ||||
| 		} else { | ||||
| 			log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	canAuth, options := client.Extension("AUTH") | ||||
| 	if len(opts.User) > 0 { | ||||
| 		if !canAuth { | ||||
| 			return fmt.Errorf("SMTP server does not support AUTH, but credentials provided") | ||||
| 		} | ||||
| 
 | ||||
| 		var auth smtp.Auth | ||||
| 
 | ||||
| 		if strings.Contains(options, "CRAM-MD5") { | ||||
| 			auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd) | ||||
| 		} else if strings.Contains(options, "PLAIN") { | ||||
| 			auth = smtp.PlainAuth("", opts.User, opts.Passwd, host) | ||||
| 		} else if strings.Contains(options, "LOGIN") { | ||||
| 			// Patch for AUTH LOGIN | ||||
| 			auth = LoginAuth(opts.User, opts.Passwd) | ||||
| 		} else if strings.Contains(options, "NTLM") { | ||||
| 			auth = NtlmAuth(opts.User, opts.Passwd) | ||||
| 		} | ||||
| 
 | ||||
| 		if auth != nil { | ||||
| 			if err = client.Auth(auth); err != nil { | ||||
| 				return fmt.Errorf("failed to authenticate SMTP: %w", err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.OverrideEnvelopeFrom { | ||||
| 		if err = client.Mail(opts.EnvelopeFrom); err != nil { | ||||
| 			return fmt.Errorf("failed to issue MAIL command: %w", err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if err = client.Mail(from); err != nil { | ||||
| 			return fmt.Errorf("failed to issue MAIL command: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, rec := range to { | ||||
| 		if err = client.Rcpt(rec); err != nil { | ||||
| 			return fmt.Errorf("failed to issue RCPT command: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	w, err := client.Data() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to issue DATA command: %w", err) | ||||
| 	} else if _, err = msg.WriteTo(w); err != nil { | ||||
| 		return fmt.Errorf("SMTP write failed: %w", err) | ||||
| 	} else if err = w.Close(); err != nil { | ||||
| 		return fmt.Errorf("SMTP close failed: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return client.Quit() | ||||
| } | ||||
| 
 | ||||
| // Sender sendmail mail sender | ||||
| type sendmailSender struct{} | ||||
| 
 | ||||
| // Send send email | ||||
| func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error { | ||||
| 	var err error | ||||
| 	var closeError error | ||||
| 	var waitError error | ||||
| 
 | ||||
| 	envelopeFrom := from | ||||
| 	if setting.MailService.OverrideEnvelopeFrom { | ||||
| 		envelopeFrom = setting.MailService.EnvelopeFrom | ||||
| 	} | ||||
| 
 | ||||
| 	args := []string{"-f", envelopeFrom, "-i"} | ||||
| 	args = append(args, setting.MailService.SendmailArgs...) | ||||
| 	args = append(args, to...) | ||||
| 	log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args) | ||||
| 
 | ||||
| 	desc := fmt.Sprintf("SendMail: %s %v", setting.MailService.SendmailPath, args) | ||||
| 
 | ||||
| 	ctx, _, finished := process.GetManager().AddContextTimeout(graceful.GetManager().HammerContext(), setting.MailService.SendmailTimeout, desc) | ||||
| 	defer finished() | ||||
| 
 | ||||
| 	cmd := exec.CommandContext(ctx, setting.MailService.SendmailPath, args...) | ||||
| 	pipe, err := cmd.StdinPipe() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	process.SetSysProcAttribute(cmd) | ||||
| 
 | ||||
| 	if err = cmd.Start(); err != nil { | ||||
| 		_ = pipe.Close() | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.MailService.SendmailConvertCRLF { | ||||
| 		buf := &strings.Builder{} | ||||
| 		_, err = msg.WriteTo(buf) | ||||
| 		if err == nil { | ||||
| 			_, err = strings.NewReplacer("\r\n", "\n").WriteString(pipe, buf.String()) | ||||
| 		} | ||||
| 	} else { | ||||
| 		_, err = msg.WriteTo(pipe) | ||||
| 	} | ||||
| 
 | ||||
| 	// we MUST close the pipe or sendmail will hang waiting for more of the message | ||||
| 	// Also we should wait on our sendmail command even if something fails | ||||
| 	closeError = pipe.Close() | ||||
| 	waitError = cmd.Wait() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} else if closeError != nil { | ||||
| 		return closeError | ||||
| 	} | ||||
| 	return waitError | ||||
| } | ||||
| 
 | ||||
| // Sender sendmail mail sender | ||||
| type dummySender struct{} | ||||
| 
 | ||||
| // Send send email | ||||
| func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error { | ||||
| 	buf := bytes.Buffer{} | ||||
| 	if _, err := msg.WriteTo(&buf); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	log.Debug("Mail From: %s To: %v Body: %s", from, to, buf.String()) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var mailQueue *queue.WorkerPoolQueue[*Message] | ||||
| 
 | ||||
| // Sender sender for sending mail synchronously | ||||
| var Sender gomail.Sender | ||||
| // sender sender for sending mail synchronously | ||||
| var sender sender_service.Sender | ||||
| 
 | ||||
| // NewContext start mail queue service | ||||
| func NewContext(ctx context.Context) { | ||||
| @ -406,20 +36,20 @@ func NewContext(ctx context.Context) { | ||||
| 
 | ||||
| 	switch setting.MailService.Protocol { | ||||
| 	case "sendmail": | ||||
| 		Sender = &sendmailSender{} | ||||
| 		sender = &sender_service.SendmailSender{} | ||||
| 	case "dummy": | ||||
| 		Sender = &dummySender{} | ||||
| 		sender = &sender_service.DummySender{} | ||||
| 	default: | ||||
| 		Sender = &smtpSender{} | ||||
| 		sender = &sender_service.SMTPSender{} | ||||
| 	} | ||||
| 
 | ||||
| 	subjectTemplates, bodyTemplates = templates.Mailer(ctx) | ||||
| 
 | ||||
| 	mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*Message) []*Message { | ||||
| 	mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*sender_service.Message) []*sender_service.Message { | ||||
| 		for _, msg := range items { | ||||
| 			gomailMsg := msg.ToMessage() | ||||
| 			log.Trace("New e-mail sending request %s: %s", gomailMsg.GetHeader("To"), msg.Info) | ||||
| 			if err := gomail.Send(Sender, gomailMsg); err != nil { | ||||
| 			if err := sender_service.Send(sender, msg); err != nil { | ||||
| 				log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetHeader("To"), msg.Info, err) | ||||
| 			} else { | ||||
| 				log.Trace("E-mails sent %s: %s", gomailMsg.GetHeader("To"), msg.Info) | ||||
| @ -436,7 +66,7 @@ func NewContext(ctx context.Context) { | ||||
| // SendAsync send emails asynchronously (make it mockable) | ||||
| var SendAsync = sendAsync | ||||
| 
 | ||||
| func sendAsync(msgs ...*Message) { | ||||
| func sendAsync(msgs ...*sender_service.Message) { | ||||
| 	if setting.MailService == nil { | ||||
| 		log.Error("Mailer: SendAsync is being invoked but mail service hasn't been initialized") | ||||
| 		return | ||||
|  | ||||
							
								
								
									
										26
									
								
								services/mailer/sender/dummy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								services/mailer/sender/dummy.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package sender | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"io" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| ) | ||||
| 
 | ||||
| // DummySender Sender sendmail mail sender | ||||
| type DummySender struct{} | ||||
| 
 | ||||
| var _ Sender = &DummySender{} | ||||
| 
 | ||||
| // Send send email | ||||
| func (s *DummySender) Send(from string, to []string, msg io.WriterTo) error { | ||||
| 	buf := bytes.Buffer{} | ||||
| 	if _, err := msg.WriteTo(&buf); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	log.Debug("Mail From: %s To: %v Body: %s", from, to, buf.String()) | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										112
									
								
								services/mailer/sender/message.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								services/mailer/sender/message.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package sender | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"hash/fnv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 
 | ||||
| 	"github.com/jaytaylor/html2text" | ||||
| 	"gopkg.in/gomail.v2" | ||||
| ) | ||||
| 
 | ||||
| // Message mail body and log info | ||||
| type Message struct { | ||||
| 	Info            string // Message information for log purpose. | ||||
| 	FromAddress     string | ||||
| 	FromDisplayName string | ||||
| 	To              string // Use only one recipient to prevent leaking of addresses | ||||
| 	ReplyTo         string | ||||
| 	Subject         string | ||||
| 	Date            time.Time | ||||
| 	Body            string | ||||
| 	Headers         map[string][]string | ||||
| } | ||||
| 
 | ||||
| // ToMessage converts a Message to gomail.Message | ||||
| func (m *Message) ToMessage() *gomail.Message { | ||||
| 	msg := gomail.NewMessage() | ||||
| 	msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) | ||||
| 	msg.SetHeader("To", m.To) | ||||
| 	if m.ReplyTo != "" { | ||||
| 		msg.SetHeader("Reply-To", m.ReplyTo) | ||||
| 	} | ||||
| 	for header := range m.Headers { | ||||
| 		msg.SetHeader(header, m.Headers[header]...) | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.MailService.SubjectPrefix != "" { | ||||
| 		msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) | ||||
| 	} else { | ||||
| 		msg.SetHeader("Subject", m.Subject) | ||||
| 	} | ||||
| 	msg.SetDateHeader("Date", m.Date) | ||||
| 	msg.SetHeader("X-Auto-Response-Suppress", "All") | ||||
| 
 | ||||
| 	plainBody, err := html2text.FromString(m.Body) | ||||
| 	if err != nil || setting.MailService.SendAsPlainText { | ||||
| 		if strings.Contains(base.TruncateString(m.Body, 100), "<html>") { | ||||
| 			log.Warn("Mail contains HTML but configured to send as plain text.") | ||||
| 		} | ||||
| 		msg.SetBody("text/plain", plainBody) | ||||
| 	} else { | ||||
| 		msg.SetBody("text/plain", plainBody) | ||||
| 		msg.AddAlternative("text/html", m.Body) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(msg.GetHeader("Message-ID")) == 0 { | ||||
| 		msg.SetHeader("Message-ID", m.generateAutoMessageID()) | ||||
| 	} | ||||
| 
 | ||||
| 	for k, v := range setting.MailService.OverrideHeader { | ||||
| 		if len(msg.GetHeader(k)) != 0 { | ||||
| 			log.Debug("Mailer override header '%s' as per config", k) | ||||
| 		} | ||||
| 		msg.SetHeader(k, v...) | ||||
| 	} | ||||
| 
 | ||||
| 	return msg | ||||
| } | ||||
| 
 | ||||
| // SetHeader adds additional headers to a message | ||||
| func (m *Message) SetHeader(field string, value ...string) { | ||||
| 	m.Headers[field] = value | ||||
| } | ||||
| 
 | ||||
| func (m *Message) generateAutoMessageID() string { | ||||
| 	dateMs := m.Date.UnixNano() / 1e6 | ||||
| 	h := fnv.New64() | ||||
| 	if len(m.To) > 0 { | ||||
| 		_, _ = h.Write([]byte(m.To)) | ||||
| 	} | ||||
| 	_, _ = h.Write([]byte(m.Subject)) | ||||
| 	_, _ = h.Write([]byte(m.Body)) | ||||
| 	return fmt.Sprintf("<autogen-%d-%016x@%s>", dateMs, h.Sum64(), setting.Domain) | ||||
| } | ||||
| 
 | ||||
| // NewMessageFrom creates new mail message object with custom From header. | ||||
| func NewMessageFrom(to, fromDisplayName, fromAddress, subject, body string) *Message { | ||||
| 	log.Trace("NewMessageFrom (body):\n%s", body) | ||||
| 
 | ||||
| 	return &Message{ | ||||
| 		FromAddress:     fromAddress, | ||||
| 		FromDisplayName: fromDisplayName, | ||||
| 		To:              to, | ||||
| 		Subject:         subject, | ||||
| 		Date:            time.Now(), | ||||
| 		Body:            body, | ||||
| 		Headers:         map[string][]string{}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // NewMessage creates new mail message object with default From header. | ||||
| func NewMessage(to, subject, body string) *Message { | ||||
| 	return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body) | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| // Copyright 2021 The Gogs Authors. All rights reserved. | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package mailer | ||||
| package sender | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
							
								
								
									
										27
									
								
								services/mailer/sender/sender.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								services/mailer/sender/sender.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package sender | ||||
| 
 | ||||
| import ( | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 
 | ||||
| 	"gopkg.in/gomail.v2" | ||||
| ) | ||||
| 
 | ||||
| type Sender gomail.Sender | ||||
| 
 | ||||
| var Send = send | ||||
| 
 | ||||
| func send(sender Sender, msgs ...*Message) error { | ||||
| 	if setting.MailService == nil { | ||||
| 		log.Error("Mailer: Send is being invoked but mail service hasn't been initialized") | ||||
| 		return nil | ||||
| 	} | ||||
| 	goMsgs := []*gomail.Message{} | ||||
| 	for _, msg := range msgs { | ||||
| 		goMsgs = append(goMsgs, msg.ToMessage()) | ||||
| 	} | ||||
| 	return gomail.Send(sender, goMsgs...) | ||||
| } | ||||
							
								
								
									
										76
									
								
								services/mailer/sender/sendmail.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								services/mailer/sender/sendmail.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package sender | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/process" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
| 
 | ||||
| // SendmailSender Sender sendmail mail sender | ||||
| type SendmailSender struct{} | ||||
| 
 | ||||
| var _ Sender = &SendmailSender{} | ||||
| 
 | ||||
| // Send send email | ||||
| func (s *SendmailSender) Send(from string, to []string, msg io.WriterTo) error { | ||||
| 	var err error | ||||
| 	var closeError error | ||||
| 	var waitError error | ||||
| 
 | ||||
| 	envelopeFrom := from | ||||
| 	if setting.MailService.OverrideEnvelopeFrom { | ||||
| 		envelopeFrom = setting.MailService.EnvelopeFrom | ||||
| 	} | ||||
| 
 | ||||
| 	args := []string{"-f", envelopeFrom, "-i"} | ||||
| 	args = append(args, setting.MailService.SendmailArgs...) | ||||
| 	args = append(args, to...) | ||||
| 	log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args) | ||||
| 
 | ||||
| 	desc := fmt.Sprintf("SendMail: %s %v", setting.MailService.SendmailPath, args) | ||||
| 
 | ||||
| 	ctx, _, finished := process.GetManager().AddContextTimeout(graceful.GetManager().HammerContext(), setting.MailService.SendmailTimeout, desc) | ||||
| 	defer finished() | ||||
| 
 | ||||
| 	cmd := exec.CommandContext(ctx, setting.MailService.SendmailPath, args...) | ||||
| 	pipe, err := cmd.StdinPipe() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	process.SetSysProcAttribute(cmd) | ||||
| 
 | ||||
| 	if err = cmd.Start(); err != nil { | ||||
| 		_ = pipe.Close() | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.MailService.SendmailConvertCRLF { | ||||
| 		buf := &strings.Builder{} | ||||
| 		_, err = msg.WriteTo(buf) | ||||
| 		if err == nil { | ||||
| 			_, err = strings.NewReplacer("\r\n", "\n").WriteString(pipe, buf.String()) | ||||
| 		} | ||||
| 	} else { | ||||
| 		_, err = msg.WriteTo(pipe) | ||||
| 	} | ||||
| 
 | ||||
| 	// we MUST close the pipe or sendmail will hang waiting for more of the message | ||||
| 	// Also we should wait on our sendmail command even if something fails | ||||
| 	closeError = pipe.Close() | ||||
| 	waitError = cmd.Wait() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} else if closeError != nil { | ||||
| 		return closeError | ||||
| 	} | ||||
| 	return waitError | ||||
| } | ||||
							
								
								
									
										150
									
								
								services/mailer/sender/smtp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								services/mailer/sender/smtp.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package sender | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net" | ||||
| 	"net/smtp" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
| 
 | ||||
| // SMTPSender Sender SMTP mail sender | ||||
| type SMTPSender struct{} | ||||
| 
 | ||||
| var _ Sender = &SMTPSender{} | ||||
| 
 | ||||
| // Send send email | ||||
| func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error { | ||||
| 	opts := setting.MailService | ||||
| 
 | ||||
| 	var network string | ||||
| 	var address string | ||||
| 	if opts.Protocol == "smtp+unix" { | ||||
| 		network = "unix" | ||||
| 		address = opts.SMTPAddr | ||||
| 	} else { | ||||
| 		network = "tcp" | ||||
| 		address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort) | ||||
| 	} | ||||
| 
 | ||||
| 	conn, err := net.Dial(network, address) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to establish network connection to SMTP server: %w", err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	var tlsconfig *tls.Config | ||||
| 	if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" { | ||||
| 		tlsconfig = &tls.Config{ | ||||
| 			InsecureSkipVerify: opts.ForceTrustServerCert, | ||||
| 			ServerName:         opts.SMTPAddr, | ||||
| 		} | ||||
| 
 | ||||
| 		if opts.UseClientCert { | ||||
| 			cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("could not load SMTP client certificate: %w", err) | ||||
| 			} | ||||
| 			tlsconfig.Certificates = []tls.Certificate{cert} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.Protocol == "smtps" { | ||||
| 		conn = tls.Client(conn, tlsconfig) | ||||
| 	} | ||||
| 
 | ||||
| 	host := "localhost" | ||||
| 	if opts.Protocol == "smtp+unix" { | ||||
| 		host = opts.SMTPAddr | ||||
| 	} | ||||
| 	client, err := smtp.NewClient(conn, host) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("could not initiate SMTP session: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.EnableHelo { | ||||
| 		hostname := opts.HeloHostname | ||||
| 		if len(hostname) == 0 { | ||||
| 			hostname, err = os.Hostname() | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("could not retrieve system hostname: %w", err) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if err = client.Hello(hostname); err != nil { | ||||
| 			return fmt.Errorf("failed to issue HELO command: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.Protocol == "smtp+starttls" { | ||||
| 		hasStartTLS, _ := client.Extension("STARTTLS") | ||||
| 		if hasStartTLS { | ||||
| 			if err = client.StartTLS(tlsconfig); err != nil { | ||||
| 				return fmt.Errorf("failed to start TLS connection: %w", err) | ||||
| 			} | ||||
| 		} else { | ||||
| 			log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	canAuth, options := client.Extension("AUTH") | ||||
| 	if len(opts.User) > 0 { | ||||
| 		if !canAuth { | ||||
| 			return fmt.Errorf("SMTP server does not support AUTH, but credentials provided") | ||||
| 		} | ||||
| 
 | ||||
| 		var auth smtp.Auth | ||||
| 
 | ||||
| 		if strings.Contains(options, "CRAM-MD5") { | ||||
| 			auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd) | ||||
| 		} else if strings.Contains(options, "PLAIN") { | ||||
| 			auth = smtp.PlainAuth("", opts.User, opts.Passwd, host) | ||||
| 		} else if strings.Contains(options, "LOGIN") { | ||||
| 			// Patch for AUTH LOGIN | ||||
| 			auth = LoginAuth(opts.User, opts.Passwd) | ||||
| 		} else if strings.Contains(options, "NTLM") { | ||||
| 			auth = NtlmAuth(opts.User, opts.Passwd) | ||||
| 		} | ||||
| 
 | ||||
| 		if auth != nil { | ||||
| 			if err = client.Auth(auth); err != nil { | ||||
| 				return fmt.Errorf("failed to authenticate SMTP: %w", err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.OverrideEnvelopeFrom { | ||||
| 		if err = client.Mail(opts.EnvelopeFrom); err != nil { | ||||
| 			return fmt.Errorf("failed to issue MAIL command: %w", err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if err = client.Mail(from); err != nil { | ||||
| 			return fmt.Errorf("failed to issue MAIL command: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, rec := range to { | ||||
| 		if err = client.Rcpt(rec); err != nil { | ||||
| 			return fmt.Errorf("failed to issue RCPT command: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	w, err := client.Data() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to issue DATA command: %w", err) | ||||
| 	} else if _, err = msg.WriteTo(w); err != nil { | ||||
| 		return fmt.Errorf("SMTP write failed: %w", err) | ||||
| 	} else if err = w.Close(); err != nil { | ||||
| 		return fmt.Errorf("SMTP close failed: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return client.Quit() | ||||
| } | ||||
							
								
								
									
										69
									
								
								services/mailer/sender/smtp_auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								services/mailer/sender/smtp_auth.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package sender | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/smtp" | ||||
| 
 | ||||
| 	"github.com/Azure/go-ntlmssp" | ||||
| ) | ||||
| 
 | ||||
| type loginAuth struct { | ||||
| 	username, password string | ||||
| } | ||||
| 
 | ||||
| // LoginAuth SMTP AUTH LOGIN Auth Handler | ||||
| func LoginAuth(username, password string) smtp.Auth { | ||||
| 	return &loginAuth{username, password} | ||||
| } | ||||
| 
 | ||||
| // Start start SMTP login auth | ||||
| func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { | ||||
| 	return "LOGIN", []byte{}, nil | ||||
| } | ||||
| 
 | ||||
| // Next next step of SMTP login auth | ||||
| func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { | ||||
| 	if more { | ||||
| 		switch string(fromServer) { | ||||
| 		case "Username:": | ||||
| 			return []byte(a.username), nil | ||||
| 		case "Password:": | ||||
| 			return []byte(a.password), nil | ||||
| 		default: | ||||
| 			return nil, fmt.Errorf("unknown fromServer: %s", string(fromServer)) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| type ntlmAuth struct { | ||||
| 	username, password, domain string | ||||
| 	domainNeeded               bool | ||||
| } | ||||
| 
 | ||||
| // NtlmAuth SMTP AUTH NTLM Auth Handler | ||||
| func NtlmAuth(username, password string) smtp.Auth { | ||||
| 	user, domain, domainNeeded := ntlmssp.GetDomain(username) | ||||
| 	return &ntlmAuth{user, password, domain, domainNeeded} | ||||
| } | ||||
| 
 | ||||
| // Start starts SMTP NTLM Auth | ||||
| func (a *ntlmAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { | ||||
| 	negotiateMessage, err := ntlmssp.NewNegotiateMessage(a.domain, "") | ||||
| 	return "NTLM", negotiateMessage, err | ||||
| } | ||||
| 
 | ||||
| // Next next step of SMTP ntlm auth | ||||
| func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) { | ||||
| 	if more { | ||||
| 		if len(fromServer) == 0 { | ||||
| 			return nil, fmt.Errorf("ntlm ChallengeMessage is empty") | ||||
| 		} | ||||
| 		authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded) | ||||
| 		return authenticateMessage, err | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
| @ -19,11 +19,11 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/services/mailer/incoming" | ||||
| 	incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload" | ||||
| 	sender_service "code.gitea.io/gitea/services/mailer/sender" | ||||
| 	token_service "code.gitea.io/gitea/services/mailer/token" | ||||
| 	"code.gitea.io/gitea/tests" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"gopkg.in/gomail.v2" | ||||
| ) | ||||
| 
 | ||||
| func TestIncomingEmail(t *testing.T) { | ||||
| @ -189,11 +189,15 @@ func TestIncomingEmail(t *testing.T) { | ||||
| 				token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload) | ||||
| 				assert.NoError(t, err) | ||||
| 
 | ||||
| 				msg := gomail.NewMessage() | ||||
| 				msg.SetHeader("To", strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)) | ||||
| 				msg.SetHeader("From", user.Email) | ||||
| 				msg.SetBody("text/plain", token) | ||||
| 				err = gomail.Send(&smtpTestSender{}, msg) | ||||
| 				msg := sender_service.NewMessageFrom( | ||||
| 					strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1), | ||||
| 					"", | ||||
| 					user.Email, | ||||
| 					"", | ||||
| 					token, | ||||
| 				) | ||||
| 
 | ||||
| 				err = sender_service.Send(&smtpTestSender{}, msg) | ||||
| 				assert.NoError(t, err) | ||||
| 
 | ||||
| 				assert.Eventually(t, func() bool { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user