package email import ( "bytes" "context" "crypto/tls" "errors" "fmt" "io" "net" "net/smtp" "time" ) // SendRawContext sends caller-built RFC/MIME bytes without rewriting headers, // boundaries, or Message-ID. The supplied context drives dialing, TLS // handshakes, and cancellation of blocked SMTP I/O. func (service *EmailService) SendRawContext(ctx context.Context, message RawMessage) error { if service == nil { return permanentError("validate configuration", errors.New("email service is nil")) } if ctx == nil { return permanentError("validate context", errors.New("context is nil")) } if err := ctx.Err(); err != nil { return classifyError(ctx, "begin SMTP delivery", err, ErrorKindCanceled) } if service.configurationErr != nil { return service.configurationErr } if service.sendRawHook != nil { return service.sendRawHook(ctx, message) } envelope, err := service.resolveRawMessage(message) if err != nil { return permanentError("validate raw message", err) } address := net.JoinHostPort(service.host, service.port) rawConnection, err := service.dialContext(ctx, "tcp", address) if err != nil { return classifyError(ctx, "dial SMTP", err, ErrorKindTransient) } defer rawConnection.Close() // Closing the actual socket is what interrupts an SMTP command blocked in // net/textproto. No detached send goroutine is used. stopCancellation := context.AfterFunc(ctx, func() { _ = rawConnection.SetDeadline(time.Now()) _ = rawConnection.Close() }) defer stopCancellation() connection := rawConnection if service.tlsMode == TLSModeImplicit { if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set implicit TLS deadline", err, ErrorKindTransient) } secureConnection := tls.Client(connection, service.tlsConfig()) if err := secureConnection.HandshakeContext(ctx); err != nil { return classifyError(ctx, "implicit TLS handshake", err, ErrorKindTransient) } connection = secureConnection } if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set SMTP greeting deadline", err, ErrorKindTransient) } client, err := smtp.NewClient(connection, service.host) if err != nil { return classifyError(ctx, "read SMTP greeting", err, ErrorKindTransient) } defer client.Close() if err := service.hello(ctx, connection, client); err != nil { return err } if service.tlsMode == TLSModeSTARTTLS { if supported, _ := client.Extension("STARTTLS"); !supported { return permanentError("start TLS", errors.New("SMTP server does not advertise STARTTLS")) } if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set STARTTLS deadline", err, ErrorKindTransient) } if err := client.StartTLS(service.tlsConfig()); err != nil { return classifyError(ctx, "start TLS", err, ErrorKindTransient) } } if service.auth != nil { if service.tlsMode == TLSModeNone { return permanentError("authenticate SMTP", errors.New("authentication requires TLS")) } if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set SMTP authentication deadline", err, ErrorKindTransient) } if err := client.Auth(service.auth); err != nil { return classifyError(ctx, "authenticate SMTP", err, ErrorKindPermanent) } } if err := service.smtpCommand(ctx, connection, "MAIL FROM", func() error { return client.Mail(envelope.from) }); err != nil { return err } for _, recipient := range envelope.recipients { if err := service.smtpCommand(ctx, connection, "RCPT TO", func() error { return client.Rcpt(recipient) }); err != nil { return err } } if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set SMTP DATA deadline", err, ErrorKindTransient) } writer, err := client.Data() if err != nil { return classifyError(ctx, "begin SMTP DATA", err, ErrorKindPermanent) } if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set message write deadline", err, ErrorKindTransient) } written, err := io.Copy(writer, bytes.NewReader(envelope.data)) if err != nil { return classifyError(ctx, "write SMTP DATA", err, ErrorKindTransient) } if written != int64(len(envelope.data)) { return classifyError(ctx, "write SMTP DATA", io.ErrShortWrite, ErrorKindTransient) } if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set SMTP acknowledgement deadline", err, ErrorKindTransient) } if err := writer.Close(); err != nil { // The server may already have accepted DATA even when its final response // is lost. Retrying can therefore duplicate delivery. return classifyError(ctx, "finish SMTP DATA", err, ErrorKindTransient) } if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set SMTP quit deadline", err, ErrorKindTransient) } if err := client.Quit(); err != nil { return classifyError(ctx, "quit SMTP", err, ErrorKindTransient) } return nil } type resolvedRawMessage struct { from string recipients []string data []byte } func (service *EmailService) resolveRawMessage(message RawMessage) (resolvedRawMessage, error) { if len(message.EnvelopeRecipients) > 0 && message.To != "" { return resolvedRawMessage{}, errors.New("canonical and deprecated recipient fields cannot both be set") } if len(message.Data) > 0 && message.Body != "" { return resolvedRawMessage{}, errors.New("canonical and deprecated message fields cannot both be set") } from := message.EnvelopeFrom if from == "" { from = service.from } recipients := append([]string(nil), message.EnvelopeRecipients...) if len(recipients) == 0 && message.To != "" { recipients = []string{message.To} } data := append([]byte(nil), message.Data...) if len(data) == 0 && message.Body != "" { data = []byte(message.Body) } if err := validateEnvelopeAddress(from); err != nil { return resolvedRawMessage{}, fmt.Errorf("envelope sender: %w", err) } if len(recipients) == 0 || len(recipients) > maximumEnvelopeRecipients { return resolvedRawMessage{}, errors.New("envelope recipient count is invalid") } for _, recipient := range recipients { if err := validateEnvelopeAddress(recipient); err != nil { return resolvedRawMessage{}, fmt.Errorf("envelope recipient: %w", err) } } if len(data) == 0 { return resolvedRawMessage{}, errors.New("raw RFC message is empty") } return resolvedRawMessage{from: from, recipients: recipients, data: data}, nil } func (service *EmailService) hello(ctx context.Context, connection net.Conn, client *smtp.Client) error { if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set SMTP hello deadline", err, ErrorKindTransient) } if err := client.Hello("localhost"); err != nil { return classifyError(ctx, "SMTP hello", err, ErrorKindTransient) } return nil } func (service *EmailService) smtpCommand(ctx context.Context, connection net.Conn, operation string, command func() error) error { if err := service.setDeadline(connection, ctx); err != nil { return classifyError(ctx, "set "+operation+" deadline", err, ErrorKindTransient) } if err := command(); err != nil { return classifyError(ctx, operation, err, ErrorKindPermanent) } return nil } func (service *EmailService) setDeadline(connection net.Conn, ctx context.Context) error { deadline := time.Now().Add(service.operationTimeout) if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) { deadline = contextDeadline } return connection.SetDeadline(deadline) } func (service *EmailService) tlsConfig() *tls.Config { return &tls.Config{ MinVersion: tls.VersionTLS12, ServerName: service.host, RootCAs: service.rootCAs, } }