diff --git a/.changelog/44214.txt b/.changelog/44214.txt new file mode 100644 index 000000000000..c89ba2066647 --- /dev/null +++ b/.changelog/44214.txt @@ -0,0 +1,3 @@ +```release-note:new-action +aws_ses_send_email +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 026dbb81080e..ae13c0637878 100644 --- a/go.mod +++ b/go.mod @@ -289,7 +289,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hcl/v2 v2.23.0 + github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-framework v1.16.0 github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 @@ -300,7 +300,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.21.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 - github.com/hashicorp/terraform-plugin-testing v1.13.3 + github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247 github.com/jaswdr/faker/v2 v2.8.0 github.com/jmespath/go-jmespath v0.4.0 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -349,7 +349,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.23.0 // indirect + github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 // indirect github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect @@ -368,7 +368,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/zclconf/go-cty v1.16.4 // indirect + github.com/zclconf/go-cty v1.17.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect diff --git a/go.sum b/go.sum index 45428faddc78..fa03a69bd1c7 100644 --- a/go.sum +++ b/go.sum @@ -661,12 +661,12 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= -github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= -github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= -github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90fcAqw0Qmv4vY7zL4jEKgKarHmOnNN6SjTY68eLKGA= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-framework v1.16.0 h1:tP0f+yJg0Z672e7levixDe5EpWwrTrNryPM9kDMYIpE= @@ -685,8 +685,8 @@ github.com/hashicorp/terraform-plugin-mux v0.21.0 h1:QsEYnzSD2c3zT8zUrUGqaFGhV/Z github.com/hashicorp/terraform-plugin-mux v0.21.0/go.mod h1:Qpt8+6AD7NmL0DS7ASkN0EXpDQ2J/FnnIgeUr1tzr5A= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= -github.com/hashicorp/terraform-plugin-testing v1.13.3 h1:QLi/khB8Z0a5L54AfPrHukFpnwsGL8cwwswj4RZduCo= -github.com/hashicorp/terraform-plugin-testing v1.13.3/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE= +github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247 h1:lA6ofPwmCXAX7J7kVP9t/WMU5+eA4e9YvJUiRLPdENw= +github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247/go.mod h1:4r/7cxl1mpskfALcq58Iyu5aPiTSco8SVrKkcLyP5g4= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -790,8 +790,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE= -github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/internal/service/ses/send_email_action.go b/internal/service/ses/send_email_action.go new file mode 100644 index 000000000000..c084da17f860 --- /dev/null +++ b/internal/service/ses/send_email_action.go @@ -0,0 +1,199 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ses + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ses" + awstypes "github.com/aws/aws-sdk-go-v2/service/ses/types" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @Action(aws_ses_send_email, name="Send Email") +func newSendEmailAction(_ context.Context) (action.ActionWithConfigure, error) { + return &sendEmailAction{}, nil +} + +var ( + _ action.Action = (*sendEmailAction)(nil) +) + +type sendEmailAction struct { + framework.ActionWithModel[sendEmailActionModel] +} + +type sendEmailActionModel struct { + framework.WithRegionModel + Source types.String `tfsdk:"source"` + ToAddresses fwtypes.ListValueOf[types.String] `tfsdk:"to_addresses"` + CcAddresses fwtypes.ListValueOf[types.String] `tfsdk:"cc_addresses"` + BccAddresses fwtypes.ListValueOf[types.String] `tfsdk:"bcc_addresses"` + Subject types.String `tfsdk:"subject"` + TextBody types.String `tfsdk:"text_body"` + HtmlBody types.String `tfsdk:"html_body"` + ReplyToAddresses fwtypes.ListValueOf[types.String] `tfsdk:"reply_to_addresses"` + ReturnPath types.String `tfsdk:"return_path"` +} + +func (a *sendEmailAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Sends an email using Amazon SES. This action allows for imperative email sending with full control over recipients, content, and formatting.", + Attributes: map[string]schema.Attribute{ + names.AttrSource: schema.StringAttribute{ + Description: "The email address that is sending the email. This address must be either individually verified with Amazon SES, or from a domain that has been verified with Amazon SES.", + Required: true, + }, + "to_addresses": schema.ListAttribute{ + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + Description: "The To: field(s) of the message.", + Optional: true, + }, + "cc_addresses": schema.ListAttribute{ + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + Description: "The CC: field(s) of the message.", + Optional: true, + }, + "bcc_addresses": schema.ListAttribute{ + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + Description: "The BCC: field(s) of the message.", + Optional: true, + }, + "subject": schema.StringAttribute{ + Description: "The subject of the message: A short summary of the content, which will appear in the recipient's inbox.", + Required: true, + }, + "text_body": schema.StringAttribute{ + Description: "The message body in text format. Either text_body or html_body must be specified.", + Optional: true, + }, + "html_body": schema.StringAttribute{ + Description: "The message body in HTML format. Either text_body or html_body must be specified.", + Optional: true, + }, + "reply_to_addresses": schema.ListAttribute{ + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + Description: "The reply-to email address(es) for the message. If the recipient replies to the message, each reply-to address will receive the reply.", + Optional: true, + }, + "return_path": schema.StringAttribute{ + Description: "The email address that bounces and complaints will be forwarded to when feedback forwarding is enabled.", + Optional: true, + }, + }, + } +} + +func (a *sendEmailAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var config sendEmailActionModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // Validate that at least one body type is provided + if config.TextBody.IsNull() && config.HtmlBody.IsNull() { + resp.Diagnostics.AddError( + "Missing Email Body", + "Either text_body or html_body must be specified", + ) + return + } + + conn := a.Meta().SESClient(ctx) + + source := config.Source.ValueString() + subject := config.Subject.ValueString() + + tflog.Info(ctx, "Starting SES send email action", map[string]any{ + names.AttrSource: source, + "subject": subject, + "has_text_body": !config.TextBody.IsNull(), + "has_html_body": !config.HtmlBody.IsNull(), + }) + + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Sending email from %s...", source), + }) + + // Build destination + destination := &awstypes.Destination{} + if !config.ToAddresses.IsNull() { + destination.ToAddresses = fwflex.ExpandFrameworkStringValueList(ctx, config.ToAddresses) + } + if !config.CcAddresses.IsNull() { + destination.CcAddresses = fwflex.ExpandFrameworkStringValueList(ctx, config.CcAddresses) + } + if !config.BccAddresses.IsNull() { + destination.BccAddresses = fwflex.ExpandFrameworkStringValueList(ctx, config.BccAddresses) + } + + // Build message + message := &awstypes.Message{ + Subject: &awstypes.Content{ + Data: aws.String(subject), + }, + Body: &awstypes.Body{}, + } + + if !config.TextBody.IsNull() { + message.Body.Text = &awstypes.Content{ + Data: config.TextBody.ValueStringPointer(), + } + } + if !config.HtmlBody.IsNull() { + message.Body.Html = &awstypes.Content{ + Data: config.HtmlBody.ValueStringPointer(), + } + } + + // Build input + input := &ses.SendEmailInput{ + Source: aws.String(source), + Destination: destination, + Message: message, + } + + if !config.ReplyToAddresses.IsNull() { + input.ReplyToAddresses = fwflex.ExpandFrameworkStringValueList(ctx, config.ReplyToAddresses) + } + + if !config.ReturnPath.IsNull() { + input.ReturnPath = config.ReturnPath.ValueStringPointer() + } + + // Send email + output, err := conn.SendEmail(ctx, input) + if err != nil { + resp.Diagnostics.AddError( + "Failed to Send Email", + fmt.Sprintf("Could not send email from %s: %s", source, err), + ) + return + } + + messageId := aws.ToString(output.MessageId) + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Email sent successfully (Message ID: %s)", messageId), + }) + + tflog.Info(ctx, "SES send email action completed successfully", map[string]any{ + names.AttrSource: source, + "message_id": messageId, + }) +} diff --git a/internal/service/ses/send_email_action_test.go b/internal/service/ses/send_email_action_test.go new file mode 100644 index 000000000000..5d938d0884c5 --- /dev/null +++ b/internal/service/ses/send_email_action_test.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ses_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ses" + awstypes "github.com/aws/aws-sdk-go-v2/service/ses/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccSESSendEmailAction_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + testEmail := acctest.SkipIfEnvVarNotSet(t, "SES_VERIFIED_EMAIL") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SESEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SESServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccSendEmailActionConfig_basic(rName, testEmail), + Check: resource.ComposeTestCheckFunc( + testAccCheckSendEmailAction(ctx, testEmail), + ), + }, + }, + }) +} + +func TestAccSESSendEmailAction_htmlBody(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + testEmail := acctest.SkipIfEnvVarNotSet(t, "SES_VERIFIED_EMAIL") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SESEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SESServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccSendEmailActionConfig_htmlBody(rName, testEmail), + Check: resource.ComposeTestCheckFunc( + testAccCheckSendEmailAction(ctx, testEmail), + ), + }, + }, + }) +} + +func TestAccSESSendEmailAction_multipleRecipients(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + testEmail := acctest.SkipIfEnvVarNotSet(t, "SES_VERIFIED_EMAIL") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SESEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SESServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccSendEmailActionConfig_multipleRecipients(rName, testEmail), + Check: resource.ComposeTestCheckFunc( + testAccCheckSendEmailAction(ctx, testEmail), + ), + }, + }, + }) +} + +// testAccCheckSendEmailAction verifies the action can send emails +func testAccCheckSendEmailAction(ctx context.Context, sourceEmail string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).SESClient(ctx) + + // Verify the source email is verified in SES + input := &ses.GetIdentityVerificationAttributesInput{ + Identities: []string{sourceEmail}, + } + + output, err := conn.GetIdentityVerificationAttributes(ctx, input) + if err != nil { + return fmt.Errorf("Failed to get identity verification attributes: %w", err) + } + + if attrs, ok := output.VerificationAttributes[sourceEmail]; ok { + if attrs.VerificationStatus != awstypes.VerificationStatusSuccess { + return fmt.Errorf("Email %s is not verified in SES (status: %s)", sourceEmail, string(attrs.VerificationStatus)) + } + } else { + return fmt.Errorf("Email %s not found in SES identities", sourceEmail) + } + + return nil + } +} + +// Configuration functions + +func testAccSendEmailActionConfig_basic(rName, testEmail string) string { + return fmt.Sprintf(` +action "aws_ses_send_email" "test" { + config { + source = %[2]q + subject = "Test Email from %[1]s" + text_body = "This is a test email sent from Terraform action test." + to_addresses = [%[2]q] + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create] + actions = [action.aws_ses_send_email.test] + } + } +} +`, rName, testEmail) +} + +func testAccSendEmailActionConfig_htmlBody(rName, testEmail string) string { + return fmt.Sprintf(` +action "aws_ses_send_email" "test" { + config { + source = %[2]q + subject = "HTML Test Email from %[1]s" + html_body = "

Test Email

This is a test email sent from Terraform action test.

" + to_addresses = [%[2]q] + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create] + actions = [action.aws_ses_send_email.test] + } + } +} +`, rName, testEmail) +} + +func testAccSendEmailActionConfig_multipleRecipients(rName, testEmail string) string { + return fmt.Sprintf(` +action "aws_ses_send_email" "test" { + config { + source = %[2]q + subject = "Multi-recipient Test Email from %[1]s" + text_body = "This is a test email sent to multiple recipients." + to_addresses = [%[2]q] + cc_addresses = [%[2]q] + reply_to_addresses = [%[2]q] + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create] + actions = [action.aws_ses_send_email.test] + } + } +} +`, rName, testEmail) +} diff --git a/internal/service/ses/service_package_gen.go b/internal/service/ses/service_package_gen.go index 95d7fefaf004..92ab96308930 100644 --- a/internal/service/ses/service_package_gen.go +++ b/internal/service/ses/service_package_gen.go @@ -17,6 +17,17 @@ import ( type servicePackage struct{} +func (p *servicePackage) Actions(ctx context.Context) []*inttypes.ServicePackageAction { + return []*inttypes.ServicePackageAction{ + { + Factory: newSendEmailAction, + TypeName: "aws_ses_send_email", + Name: "Send Email", + Region: unique.Make(inttypes.ResourceRegionDefault()), + }, + } +} + func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*inttypes.ServicePackageFrameworkDataSource { return []*inttypes.ServicePackageFrameworkDataSource{} } diff --git a/website/docs/actions/ses_send_email.html.markdown b/website/docs/actions/ses_send_email.html.markdown new file mode 100644 index 000000000000..4f1bd36adae0 --- /dev/null +++ b/website/docs/actions/ses_send_email.html.markdown @@ -0,0 +1,176 @@ +--- +subcategory: "SES (Simple Email)" +layout: "aws" +page_title: "AWS: aws_ses_send_email" +description: |- + Sends an email using Amazon SES. +--- + +# Action: aws_ses_send_email + +~> **Note:** `aws_ses_send_email` is in beta. Its interface and behavior may change as the feature evolves, and breaking changes are possible. It is offered as a technical preview without compatibility guarantees until Terraform 1.14 is generally available. + +Sends an email using Amazon SES. This action allows for imperative email sending with full control over recipients, content, and formatting. + +For information about Amazon SES, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/). For specific information about sending emails, see the [SendEmail](https://docs.aws.amazon.com/ses/latest/APIReference/API_SendEmail.html) page in the Amazon SES API Reference. + +~> **Note:** All email addresses used must be verified in Amazon SES or belong to a verified domain. Due to the difficulty in testing, your help is important in discovering and reporting issues. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_ses_email_identity" "example" { + email = "sender@example.com" +} + +action "aws_ses_send_email" "example" { + config { + source = aws_ses_email_identity.example.email + subject = "Test Email" + text_body = "This is a test email sent from Terraform." + to_addresses = ["recipient@example.com"] + } +} + +resource "terraform_data" "example" { + input = "send-notification" + + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_ses_send_email.example] + } + } +} +``` + +### HTML Email with Multiple Recipients + +```terraform +action "aws_ses_send_email" "newsletter" { + config { + source = aws_ses_email_identity.marketing.email + subject = "Monthly Newsletter - ${formatdate("MMMM YYYY", timestamp())}" + html_body = "

Welcome!

This is our monthly newsletter.

" + to_addresses = var.subscriber_emails + cc_addresses = ["manager@example.com"] + reply_to_addresses = ["support@example.com"] + return_path = "bounces@example.com" + } +} +``` + +### Deployment Notification + +```terraform +action "aws_ses_send_email" "deploy_notification" { + config { + source = "deployments@example.com" + subject = "Deployment Complete: ${var.environment}" + text_body = "Application ${var.app_name} has been successfully deployed to ${var.environment}." + to_addresses = var.team_emails + } +} + +resource "terraform_data" "deployment" { + input = var.deployment_id + + lifecycle { + action_trigger { + events = [after_create] + actions = [action.aws_ses_send_email.deploy_notification] + } + } + + depends_on = [aws_instance.app] +} +``` + +### Alert Email with Dynamic Content + +```terraform +locals { + alert_body = templatefile("${path.module}/templates/alert.txt", { + service = var.service_name + environment = var.environment + timestamp = timestamp() + details = var.alert_details + }) +} + +action "aws_ses_send_email" "alert" { + config { + source = "alerts@example.com" + subject = "ALERT: ${var.service_name} Issue Detected" + text_body = local.alert_body + to_addresses = var.oncall_emails + cc_addresses = var.manager_emails + } +} +``` + +### Multi-format Email + +```terraform +action "aws_ses_send_email" "welcome" { + config { + source = aws_ses_email_identity.noreply.email + subject = "Welcome to ${var.company_name}!" + text_body = "Welcome! Thank you for joining us. Visit our website for more information." + html_body = templatefile("${path.module}/templates/welcome.html", { + user_name = var.user_name + company_name = var.company_name + website_url = var.website_url + }) + to_addresses = [var.user_email] + } +} +``` + +### Conditional Email Sending + +```terraform +action "aws_ses_send_email" "conditional" { + config { + source = "notifications@example.com" + subject = var.environment == "production" ? "Production Alert" : "Test Alert" + text_body = "This is a ${var.environment} environment notification." + to_addresses = var.environment == "production" ? var.prod_emails : var.dev_emails + } +} +``` + +### Batch Processing Notification + +```terraform +action "aws_ses_send_email" "batch_complete" { + config { + source = "batch-jobs@example.com" + subject = "Batch Processing Complete - ${var.job_name}" + html_body = <<-HTML +

Batch Job Results

+

Job: ${var.job_name}

+

Records Processed: ${var.records_processed}

+

Duration: ${var.processing_duration}

+

Status: ${var.job_status}

+ HTML + to_addresses = var.admin_emails + } +} +``` + +## Argument Reference + +This action supports the following arguments: + +* `bcc_addresses` - (Optional) List of email addresses for the BCC: field of the message. Recipients in this list will receive the email but their addresses will not be visible to other recipients. +* `cc_addresses` - (Optional) List of email addresses for the CC: field of the message. Recipients in this list will receive the email and their addresses will be visible to all recipients. +* `html_body` - (Optional) Message body in HTML format. Either `text_body` or `html_body` (or both) must be specified. HTML content allows for rich formatting including links, images, and styling. +* `reply_to_addresses` - (Optional) List of reply-to email addresses for the message. If the recipient replies to the message, each reply-to address will receive the reply. If not specified, replies will go to the source address. +* `return_path` - (Optional) Email address that bounces and complaints will be forwarded to when feedback forwarding is enabled. This is useful for handling delivery failures and spam complaints. +* `source` - (Required) Email address that is sending the email. This address must be either individually verified with Amazon SES, or from a domain that has been verified with Amazon SES. +* `subject` - (Required) Subject of the message: A short summary of the content, which will appear in the recipient's inbox. +* `text_body` - (Optional) Message body in text format. Either `text_body` or `html_body` (or both) must be specified. Text format ensures compatibility with all email clients. +* `to_addresses` - (Optional) List of email addresses for the To: field of the message. These are the primary recipients of the email.