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 }