github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/pkg/emailclient/smtp_sender.go (about)

     1  // Copyright 2025 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package emailclient
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"net/smtp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/google/syzkaller/pkg/gcpsecret"
    15  	"github.com/google/syzkaller/syz-cluster/pkg/app"
    16  	"github.com/google/uuid"
    17  )
    18  
    19  type smtpSender struct {
    20  	cfg         *app.EmailConfig
    21  	projectName string // needed for querying credentials
    22  }
    23  
    24  func newSMTPSender(ctx context.Context, cfg *app.EmailConfig) (*smtpSender, error) {
    25  	project, err := gcpsecret.ProjectName(ctx)
    26  	if err != nil {
    27  		return nil, fmt.Errorf("failed to query project name: %w", err)
    28  	}
    29  	return &smtpSender{
    30  		cfg:         cfg,
    31  		projectName: project,
    32  	}, nil
    33  }
    34  
    35  // Send constructs a raw email from EmailToSend and sends it over SMTP.
    36  func (sender *smtpSender) Send(ctx context.Context, item *Email) (string, error) {
    37  	creds, err := sender.queryCredentials(ctx)
    38  	if err != nil {
    39  		return "", fmt.Errorf("failed to query credentials: %w", err)
    40  	}
    41  	msgID := fmt.Sprintf("<%s@%s>", uuid.NewString(), creds.host)
    42  	msg := rawEmail(sender.cfg, item, msgID)
    43  	auth := smtp.PlainAuth("", creds.host, creds.password, creds.host)
    44  	smtpAddr := fmt.Sprintf("%s:%d", creds.host, creds.port)
    45  	return msgID, smtp.SendMail(smtpAddr, auth, sender.cfg.SMTP.From, item.recipients(), msg)
    46  }
    47  
    48  func rawEmail(cfg *app.EmailConfig, item *Email, id string) []byte {
    49  	var msg bytes.Buffer
    50  
    51  	fmt.Fprintf(&msg, "From: %s <%s>\r\n", cfg.Name, cfg.SMTP.From)
    52  	fmt.Fprintf(&msg, "To: %s\r\n", strings.Join(item.To, ", "))
    53  	if len(item.Cc) > 0 {
    54  		fmt.Fprintf(&msg, "Cc: %s\r\n", strings.Join(item.Cc, ", "))
    55  	}
    56  	fmt.Fprintf(&msg, "Subject: %s\r\n", item.Subject)
    57  	if item.InReplyTo != "" {
    58  		inReplyTo := item.InReplyTo
    59  		if inReplyTo[0] != '<' {
    60  			inReplyTo = "<" + inReplyTo + ">"
    61  		}
    62  		fmt.Fprintf(&msg, "In-Reply-To: %s\r\n", inReplyTo)
    63  	}
    64  	if id != "" {
    65  		if id[0] != '<' {
    66  			id = "<" + id + ">"
    67  		}
    68  		fmt.Fprintf(&msg, "Message-ID: %s\r\n", id)
    69  	}
    70  	msg.WriteString("MIME-Version: 1.0\r\n")
    71  	msg.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
    72  	msg.WriteString("Content-Transfer-Encoding: 8bit\r\n")
    73  	msg.WriteString("\r\n")
    74  	msg.Write(item.Body)
    75  	return msg.Bytes()
    76  }
    77  
    78  const (
    79  	SecretSMTPHost     string = "smtp_host"
    80  	SecretSMTPPort     string = "smtp_port"
    81  	SecretSMTPUser     string = "smtp_user"
    82  	SecretSMTPPassword string = "smtp_password"
    83  )
    84  
    85  type smtpCredentials struct {
    86  	host     string
    87  	port     int
    88  	user     string
    89  	password string
    90  }
    91  
    92  func (sender *smtpSender) queryCredentials(ctx context.Context) (smtpCredentials, error) {
    93  	values := map[string]string{}
    94  	for _, key := range []string{
    95  		SecretSMTPHost, SecretSMTPPort, SecretSMTPUser, SecretSMTPPassword,
    96  	} {
    97  		var err error
    98  		values[key], err = sender.querySecret(ctx, key)
    99  		if err != nil {
   100  			return smtpCredentials{}, err
   101  		}
   102  	}
   103  	port, err := strconv.Atoi(values[SecretSMTPPort])
   104  	if err != nil {
   105  		return smtpCredentials{}, fmt.Errorf("failed to parse SMTP port: not a valid integer")
   106  	}
   107  	return smtpCredentials{
   108  		host:     values[SecretSMTPHost],
   109  		port:     port,
   110  		user:     values[SecretSMTPUser],
   111  		password: values[SecretSMTPPassword],
   112  	}, nil
   113  }
   114  
   115  func (sender *smtpSender) querySecret(ctx context.Context, key string) (string, error) {
   116  	const retries = 3
   117  	var err error
   118  	for i := 0; i < retries; i++ {
   119  		var val []byte
   120  		val, err := gcpsecret.LatestGcpSecret(ctx, sender.projectName, key)
   121  		if err == nil {
   122  			return string(val), nil
   123  		}
   124  	}
   125  	return "", fmt.Errorf("failed to query %v: %w", key, err)
   126  }