From 7db8f7cb5a64dd27644ee1c91ae6d479196cda88 Mon Sep 17 00:00:00 2001 From: Ben Zhang Date: Mon, 30 Dec 2024 18:25:08 -0800 Subject: [PATCH] Support multiple recipients (#11) This PR implements support for multiple recipients via a new `to_list` field that replaces the `to` field. This is natively supported by the `smtp.SendMail` function. --- docs/resources/email.md | 3 ++- email/provider_test.go | 8 +++++--- email/resource_email.go | 43 ++++++++++++++++++++++++++++++++++------- examples/main.tf | 4 ++-- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/docs/resources/email.md b/docs/resources/email.md index 44612fb..4ebe868 100644 --- a/docs/resources/email.md +++ b/docs/resources/email.md @@ -24,7 +24,6 @@ description: |- - `smtp_server` (String) - `smtp_username` (String) - `subject` (String) -- `to` (String) ### Optional @@ -32,7 +31,9 @@ description: |- - `from_display_name` (String) - `preamble` (String) - `reply_to` (String) +- `to` (String, Deprecated) - `to_display_name` (String) +- `to_list` (List of String) ### Read-Only diff --git a/email/provider_test.go b/email/provider_test.go index ad3cb52..0b9f836 100644 --- a/email/provider_test.go +++ b/email/provider_test.go @@ -26,7 +26,9 @@ func TestAccEmail_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckEmailExists("email_email.example"), resource.TestCheckResourceAttr( - "email_email.example", "to", "recipient@example.com"), + "email_email.example", "to_list.#", "1"), + resource.TestCheckResourceAttr( + "email_email.example", "to_list.0", "recipient@example.com"), resource.TestCheckResourceAttr( "email_email.example", "from", "sender@example.com"), resource.TestCheckResourceAttr( @@ -115,7 +117,7 @@ func TestRetryWorkflow(t *testing.T) { t.Run(test.name, func(t *testing.T) { // set test retries zero before each test sendMailInvocations = 0 - err := sendMail(test.sendMailFunc, maxRetries, "localhost", "2525", "username", "password", "from@example.com", "to@example.com", "message") + err := sendMail(test.sendMailFunc, maxRetries, "localhost", "2525", "username", "password", "from@example.com", []string{"to@example.com"}, "message") if test.expectedErr != nil { // assert that the errors are equal assert.EqualError(t, err, test.expectedErr.Error()) @@ -132,7 +134,7 @@ func TestRetryWorkflow(t *testing.T) { // `docker run --rm -it -p 3000:80 -p 2525:25 rnwood/smtp4dev:v3` const testAccEmailConfig = ` resource "email_email" "example" { - to = "recipient@example.com" + to_list = ["recipient@example.com"] from = "sender@example.com" reply_to = "reply_to@example.com" subject = "Test Subject" diff --git a/email/resource_email.go b/email/resource_email.go index 6eb89b9..c6c5482 100644 --- a/email/resource_email.go +++ b/email/resource_email.go @@ -1,11 +1,13 @@ package email import ( + "errors" "log" "math/rand" "net/smtp" "regexp" "strconv" + "strings" "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,6 +18,8 @@ type SendMailFunc func(addr string, a smtp.Auth, from string, to []string, msg [ func resourceEmail() *schema.Resource { return &schema.Resource{ + SchemaVersion: 1, + Create: resourceEmailCreate, Read: resourceEmailRead, Update: resourceEmailUpdate, @@ -23,8 +27,15 @@ func resourceEmail() *schema.Resource { Schema: map[string]*schema.Schema{ "to": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + Deprecated: "Use `to_list` instead", + }, + "to_list": &schema.Schema{ + Type: schema.TypeList, + // TODO: make this required after deprecating `to` + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, "to_display_name": &schema.Schema{ Type: schema.TypeString, @@ -94,7 +105,25 @@ func extractStatusCode(errMsg string) string { } func resourceEmailCreate(d *schema.ResourceData, m interface{}) error { - to := d.Get("to").(string) + rawToList, ok := d.GetOk("to_list") + // For backward compatibility + if !ok { + rawTo, ok := d.GetOk("to") + if !ok { + // raise exception + return errors.New("`to` or `to_list` must be specified") + } + rawToList = []interface{}{rawTo} + } + to := make([]string, len(rawToList.([]interface{}))) + for i, v := range rawToList.([]interface{}) { + to[i] = v.(string) + } + + if len(to) == 0 { + return errors.New("at least one recipient must be specified") + } + toDisplayName := d.Get("to_display_name").(string) from := d.Get("from").(string) fromDisplayName := d.Get("from_display_name").(string) @@ -109,7 +138,7 @@ func resourceEmailCreate(d *schema.ResourceData, m interface{}) error { dryRun := d.Get("dry_run").(bool) if toDisplayName == "" { - toDisplayName = to + toDisplayName = strings.Join(to, ", ") } if fromDisplayName == "" { fromDisplayName = from @@ -137,12 +166,12 @@ func resourceEmailCreate(d *schema.ResourceData, m interface{}) error { } timestamp := time.Now().Unix() - d.SetId(to + " | " + subject + " | " + strconv.FormatInt(timestamp, 10)) + d.SetId(strings.Join(to, ",") + " | " + subject + " | " + strconv.FormatInt(timestamp, 10)) return resourceEmailRead(d, m) } -func sendMail(sendEmailImpl SendMailFunc, maxRetries int, smtpServer string, smtpPort string, smtpUsername string, smtpPassword string, from string, to string, msg string) error { +func sendMail(sendEmailImpl SendMailFunc, maxRetries int, smtpServer string, smtpPort string, smtpUsername string, smtpPassword string, from string, to []string, msg string) error { // Set up a random number for exponential backoff minRandInt := 10 maxRandInt := 150 @@ -153,7 +182,7 @@ func sendMail(sendEmailImpl SendMailFunc, maxRetries int, smtpServer string, smt // send smtp email err = sendEmailImpl(smtpServer+":"+smtpPort, smtp.PlainAuth("", smtpUsername, smtpPassword, smtpServer), - from, []string{to}, []byte(msg)) + from, to, []byte(msg)) if err == nil { break diff --git a/examples/main.tf b/examples/main.tf index 7d5863e..1b622fb 100644 --- a/examples/main.tf +++ b/examples/main.tf @@ -11,7 +11,7 @@ provider "email" {} resource "email_email" "example" { - to = "infra-outreach@watonomous.ca" + to_list = ["infra-outreach@watonomous.ca"] to_display_name = "Infrastructure Outreach " from = "sentry-outgoing@watonomous.ca" from_display_name = "Sentry Outgoing " @@ -27,7 +27,7 @@ resource "email_email" "example" { } resource "email_email" "example_with_styling" { - to = "infra-outreach@watonomous.ca" + to_list = ["infra-outreach@watonomous.ca"] to_display_name = "Infrastructure Outreach " from = "sentry-outgoing@watonomous.ca" from_display_name = "Sentry Outgoing "