Skip to content

Commit

Permalink
Support multiple recipients (#11)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ben-z authored Dec 31, 2024
1 parent d8183bd commit 7db8f7c
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 13 deletions.
3 changes: 2 additions & 1 deletion docs/resources/email.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ description: |-
- `smtp_server` (String)
- `smtp_username` (String)
- `subject` (String)
- `to` (String)

### Optional

- `dry_run` (Boolean)
- `from_display_name` (String)
- `preamble` (String)
- `reply_to` (String)
- `to` (String, Deprecated)
- `to_display_name` (String)
- `to_list` (List of String)

### Read-Only

Expand Down
8 changes: 5 additions & 3 deletions email/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ func TestAccEmail_basic(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
testAccCheckEmailExists("email_email.example"),
resource.TestCheckResourceAttr(
"email_email.example", "to", "[email protected]"),
"email_email.example", "to_list.#", "1"),
resource.TestCheckResourceAttr(
"email_email.example", "to_list.0", "[email protected]"),
resource.TestCheckResourceAttr(
"email_email.example", "from", "[email protected]"),
resource.TestCheckResourceAttr(
Expand Down Expand Up @@ -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", "[email protected]", "[email protected]", "message")
err := sendMail(test.sendMailFunc, maxRetries, "localhost", "2525", "username", "password", "[email protected]", []string{"[email protected]"}, "message")
if test.expectedErr != nil {
// assert that the errors are equal
assert.EqualError(t, err, test.expectedErr.Error())
Expand All @@ -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 = "[email protected]"
to_list = ["[email protected]"]
from = "[email protected]"
reply_to = "[email protected]"
subject = "Test Subject"
Expand Down
43 changes: 36 additions & 7 deletions email/resource_email.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -16,15 +18,24 @@ 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,
Delete: resourceEmailDelete,

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,
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions examples/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ provider "email" {}


resource "email_email" "example" {
to = "[email protected]"
to_list = ["[email protected]"]
to_display_name = "Infrastructure Outreach <[email protected]>"
from = "[email protected]"
from_display_name = "Sentry Outgoing <[email protected]>"
Expand All @@ -27,7 +27,7 @@ resource "email_email" "example" {
}

resource "email_email" "example_with_styling" {
to = "[email protected]"
to_list = ["[email protected]"]
to_display_name = "Infrastructure Outreach <[email protected]>"
from = "[email protected]"
from_display_name = "Sentry Outgoing <[email protected]>"
Expand Down

0 comments on commit 7db8f7c

Please sign in to comment.