From e0d412c26f43dc416500b908d507a0264fb9b648 Mon Sep 17 00:00:00 2001 From: Hasan Kumar Date: Tue, 24 Oct 2017 16:22:28 +0530 Subject: [PATCH 1/2] Add support for SES MAIL FROM https://docs.aws.amazon.com/ses/latest/DeveloperGuide/mail-from-set.html --- aws/provider.go | 1 + aws/resource_aws_ses_domain_mail_from.go | 96 +++++++++++++++++++ aws/resource_aws_ses_domain_mail_from_test.go | 76 +++++++++++++++ website/aws.erb | 4 + .../docs/r/ses_domain_mail_from.html.markdown | 67 +++++++++++++ 5 files changed, 244 insertions(+) create mode 100644 aws/resource_aws_ses_domain_mail_from.go create mode 100644 aws/resource_aws_ses_domain_mail_from_test.go create mode 100644 website/docs/r/ses_domain_mail_from.html.markdown diff --git a/aws/provider.go b/aws/provider.go index f4e3c4f9bc0..136f776a3f8 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -429,6 +429,7 @@ func Provider() terraform.ResourceProvider { "aws_ses_active_receipt_rule_set": resourceAwsSesActiveReceiptRuleSet(), "aws_ses_domain_identity": resourceAwsSesDomainIdentity(), "aws_ses_domain_dkim": resourceAwsSesDomainDkim(), + "aws_ses_domain_mail_from": resourceAwsSesDomainMailFrom(), "aws_ses_receipt_filter": resourceAwsSesReceiptFilter(), "aws_ses_receipt_rule": resourceAwsSesReceiptRule(), "aws_ses_receipt_rule_set": resourceAwsSesReceiptRuleSet(), diff --git a/aws/resource_aws_ses_domain_mail_from.go b/aws/resource_aws_ses_domain_mail_from.go new file mode 100644 index 00000000000..8f095cf8cdc --- /dev/null +++ b/aws/resource_aws_ses_domain_mail_from.go @@ -0,0 +1,96 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ses" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSesDomainMailFrom() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSesDomainMailFromCreate, + Read: resourceAwsSesDomainMailFromRead, + Delete: resourceAwsSesDomainMailFromDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "domain": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "mail_from_domain": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsSesDomainMailFromCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sesConn + + domainName := d.Get("domain").(string) + mailFromDomain := d.Get("mail_from_domain").(string) + + createOpts := &ses.SetIdentityMailFromDomainInput{ + BehaviorOnMXFailure: aws.String("UseDefaultValue"), + Identity: aws.String(domainName), + MailFromDomain: aws.String(mailFromDomain), + } + + _, err := conn.SetIdentityMailFromDomain(createOpts) + if err != nil { + return fmt.Errorf("Error setting MAIL FROM domain: %s", err) + } + + d.SetId(domainName) + + return resourceAwsSesDomainMailFromRead(d, meta) +} + +func resourceAwsSesDomainMailFromRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sesConn + + domainName := d.Id() + d.Set("domain", domainName) + + readOpts := &ses.GetIdentityMailFromDomainAttributesInput{ + Identities: []*string{ + aws.String(domainName), + }, + } + + _, err := conn.GetIdentityMailFromDomainAttributes(readOpts) + if err != nil { + log.Printf("[WARN] Error fetching MAIL FROM domain attributes for %s: %s", d.Id(), err) + return err + } + + return nil +} + +func resourceAwsSesDomainMailFromDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sesConn + + domainName := d.Get("domain").(string) + + deleteOpts := &ses.SetIdentityMailFromDomainInput{ + BehaviorOnMXFailure: aws.String("UseDefaultValue"), + Identity: aws.String(domainName), + MailFromDomain: nil, + } + + _, err := conn.SetIdentityMailFromDomain(deleteOpts) + if err != nil { + return fmt.Errorf("Error deleting SES domain identity: %s", err) + } + + return nil +} diff --git a/aws/resource_aws_ses_domain_mail_from_test.go b/aws/resource_aws_ses_domain_mail_from_test.go new file mode 100644 index 00000000000..3ce8e5dcd79 --- /dev/null +++ b/aws/resource_aws_ses_domain_mail_from_test.go @@ -0,0 +1,76 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ses" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAwsSESDomainMailFrom_basic(t *testing.T) { + domain := fmt.Sprintf( + "%s.terraformtesting.com", + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccAwsSESDomainMailFromConfig, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsSESDomainMailFromExists("aws_ses_domain_mail_from.test"), + ), + }, + }, + }) +} + +func testAccCheckAwsSESDomainMailFromExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("SES Domain Identity not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("SES Domain Identity name not set") + } + + domain := rs.Primary.ID + conn := testAccProvider.Meta().(*AWSClient).sesConn + + params := &ses.GetIdentityMailFromDomainAttributesInput{ + Identities: []*string{ + aws.String(domain), + }, + } + + response, err := conn.GetIdentityMailFromDomainAttributes(params) + if err != nil { + return err + } + + if response.MailFromDomainAttributes[domain] == nil { + return fmt.Errorf("SES Domain MAIL FROM %s not found in AWS", domain) + } + + return nil + } +} + +const testAccAwsSESDomainMailFromConfig = ` +resource "aws_ses_domain_identity" "test" { + domain = "%s" +} +resource "aws_ses_domain_mail_from" "test" { + domain = "${aws_ses_domain_identity.test.domain}" + mail_from_domain = "bounce.${aws_ses_domain_identity.test.domain}" +} +` diff --git a/website/aws.erb b/website/aws.erb index 857dc699e5a..819bd578e87 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1325,6 +1325,10 @@ aws_ses_domain_dkim + > + aws_ses_domain_mail_from + + > aws_ses_receipt_filter diff --git a/website/docs/r/ses_domain_mail_from.html.markdown b/website/docs/r/ses_domain_mail_from.html.markdown new file mode 100644 index 00000000000..7fe2ea5deab --- /dev/null +++ b/website/docs/r/ses_domain_mail_from.html.markdown @@ -0,0 +1,67 @@ +--- +layout: "aws" +page_title: "AWS: ses_domain_mail_from" +sidebar_current: "docs-aws-resource-ses-domain-mail-from" +description: |- + Provides an SES domain MAIL FROM resource +--- + +# aws\_ses\_domain\_dkim + +Provides an SES domain MAIL FROM resource. + +Domain ownership needs to be confirmed first using [ses_domain_identity Resource](/docs/providers/aws/r/ses_domain_identity.html) + +## Argument Reference + +The following arguments are supported: + +* `domain` - (Required) Verified domain name to generate DKIM tokens for. +* `mail_from_domain` - (Required) Subdomain (of above domain) which is to be used as MAIL FROM address (Required for DMARC validation) + +## Followup Steps + +Find out more about setting MAIL FROM domain with Amazon SES in [docs](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/mail-from-set.html). + +* Create an MX record used to verify SES MAIL FROM setup + +* (Optionally) If you want your emails to pass SPF checks, you must publish an SPF record to the DNS server of the custom MAIL FROM domain. + +## Example Usage + +```hcl +resource "aws_ses_domain_identity" "example" { + domain = "example.com" +} + +resource "aws_ses_domain_mail_from" "example" { + domain = "example.com" + mail_from_domain = "bounce.example.com" +} + +resource "aws_route53_record" "example_amazonses_mail_from_mx_record" { + zone_id = "ABCDEFGHIJ123" # Change to appropriate Route53 Zone ID + name = "bounce.example.com" + type = "MX" + ttl = "600" + records = ["10 feedback-smtp.us-east-1.amazonses.com"] # Change to the region in which `aws_ses_domain_identity.example` is created +} + +# For SPF Check +resource "aws_route53_record" "example_amazonses_mail_from_mx_record" { + zone_id = "ABCDEFGHIJ123" # Change to appropriate Route53 Zone ID + name = "bounce.example.com" + type = "TXT" + ttl = "600" + records = ["v=spf1 include:amazonses.com -all"] +} + +``` + +## Import + +MAIL FROM domain can be imported using the `domain` attribute, e.g. + +``` +$ terraform import aws_ses_domain_mail_from.example example.com +``` From 428b4e6b2cc134fe2c93701800c18e4bc884414a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Feb 2018 11:50:33 -0500 Subject: [PATCH 2/2] resource/aws_ses_domain_mail: Add behavior_on_mx_failure attribute, allow updates, augment testing --- aws/resource_aws_ses_domain_mail_from.go | 40 ++++-- aws/resource_aws_ses_domain_mail_from_test.go | 114 ++++++++++++++++-- .../docs/r/ses_domain_mail_from.html.markdown | 63 +++++----- 3 files changed, 164 insertions(+), 53 deletions(-) diff --git a/aws/resource_aws_ses_domain_mail_from.go b/aws/resource_aws_ses_domain_mail_from.go index 8f095cf8cdc..c7361a60760 100644 --- a/aws/resource_aws_ses_domain_mail_from.go +++ b/aws/resource_aws_ses_domain_mail_from.go @@ -11,8 +11,9 @@ import ( func resourceAwsSesDomainMailFrom() *schema.Resource { return &schema.Resource{ - Create: resourceAwsSesDomainMailFromCreate, + Create: resourceAwsSesDomainMailFromSet, Read: resourceAwsSesDomainMailFromRead, + Update: resourceAwsSesDomainMailFromSet, Delete: resourceAwsSesDomainMailFromDelete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, @@ -27,25 +28,30 @@ func resourceAwsSesDomainMailFrom() *schema.Resource { "mail_from_domain": { Type: schema.TypeString, Required: true, - ForceNew: true, + }, + "behavior_on_mx_failure": { + Type: schema.TypeString, + Optional: true, + Default: ses.BehaviorOnMXFailureUseDefaultValue, }, }, } } -func resourceAwsSesDomainMailFromCreate(d *schema.ResourceData, meta interface{}) error { +func resourceAwsSesDomainMailFromSet(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).sesConn + behaviorOnMxFailure := d.Get("behavior_on_mx_failure").(string) domainName := d.Get("domain").(string) mailFromDomain := d.Get("mail_from_domain").(string) - createOpts := &ses.SetIdentityMailFromDomainInput{ - BehaviorOnMXFailure: aws.String("UseDefaultValue"), + input := &ses.SetIdentityMailFromDomainInput{ + BehaviorOnMXFailure: aws.String(behaviorOnMxFailure), Identity: aws.String(domainName), MailFromDomain: aws.String(mailFromDomain), } - _, err := conn.SetIdentityMailFromDomain(createOpts) + _, err := conn.SetIdentityMailFromDomain(input) if err != nil { return fmt.Errorf("Error setting MAIL FROM domain: %s", err) } @@ -59,7 +65,6 @@ func resourceAwsSesDomainMailFromRead(d *schema.ResourceData, meta interface{}) conn := meta.(*AWSClient).sesConn domainName := d.Id() - d.Set("domain", domainName) readOpts := &ses.GetIdentityMailFromDomainAttributesInput{ Identities: []*string{ @@ -67,24 +72,33 @@ func resourceAwsSesDomainMailFromRead(d *schema.ResourceData, meta interface{}) }, } - _, err := conn.GetIdentityMailFromDomainAttributes(readOpts) + out, err := conn.GetIdentityMailFromDomainAttributes(readOpts) if err != nil { - log.Printf("[WARN] Error fetching MAIL FROM domain attributes for %s: %s", d.Id(), err) + log.Printf("error fetching MAIL FROM domain attributes for %s: %s", domainName, err) return err } + d.Set("domain", domainName) + + if v, ok := out.MailFromDomainAttributes[domainName]; ok { + d.Set("behavior_on_mx_failure", v.BehaviorOnMXFailure) + d.Set("mail_from_domain", v.MailFromDomain) + } else { + d.Set("behavior_on_mx_failure", v.BehaviorOnMXFailure) + d.Set("mail_from_domain", "") + } + return nil } func resourceAwsSesDomainMailFromDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).sesConn - domainName := d.Get("domain").(string) + domainName := d.Id() deleteOpts := &ses.SetIdentityMailFromDomainInput{ - BehaviorOnMXFailure: aws.String("UseDefaultValue"), - Identity: aws.String(domainName), - MailFromDomain: nil, + Identity: aws.String(domainName), + MailFromDomain: nil, } _, err := conn.SetIdentityMailFromDomain(deleteOpts) diff --git a/aws/resource_aws_ses_domain_mail_from_test.go b/aws/resource_aws_ses_domain_mail_from_test.go index 3ce8e5dcd79..cd5ed96cf26 100644 --- a/aws/resource_aws_ses_domain_mail_from_test.go +++ b/aws/resource_aws_ses_domain_mail_from_test.go @@ -15,19 +15,72 @@ func TestAccAwsSESDomainMailFrom_basic(t *testing.T) { domain := fmt.Sprintf( "%s.terraformtesting.com", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + mailFromDomain1 := fmt.Sprintf("bounce1.%s", domain) + mailFromDomain2 := fmt.Sprintf("bounce2.%s", domain) + resourceName := "aws_ses_domain_mail_from.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSESDomainMailFromDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsSESDomainMailFromConfig(domain, mailFromDomain1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsSESDomainMailFromExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "behavior_on_mx_failure", ses.BehaviorOnMXFailureUseDefaultValue), + resource.TestCheckResourceAttr(resourceName, "domain", domain), + resource.TestCheckResourceAttr(resourceName, "mail_from_domain", mailFromDomain1), + ), + }, + { + Config: testAccAwsSESDomainMailFromConfig(domain, mailFromDomain2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsSESDomainMailFromExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "behavior_on_mx_failure", ses.BehaviorOnMXFailureUseDefaultValue), + resource.TestCheckResourceAttr(resourceName, "domain", domain), + resource.TestCheckResourceAttr(resourceName, "mail_from_domain", mailFromDomain2), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, - Providers: testAccProviders, + }) +} + +func TestAccAwsSESDomainMailFrom_behaviorOnMxFailure(t *testing.T) { + domain := fmt.Sprintf( + "%s.terraformtesting.com", + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + resourceName := "aws_ses_domain_mail_from.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSESDomainMailFromDestroy, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testAccAwsSESDomainMailFromConfig, domain), + Config: testAccAwsSESDomainMailFromConfig_behaviorOnMxFailure(domain, ses.BehaviorOnMXFailureUseDefaultValue), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsSESDomainMailFromExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "behavior_on_mx_failure", ses.BehaviorOnMXFailureUseDefaultValue), + ), + }, + { + Config: testAccAwsSESDomainMailFromConfig_behaviorOnMxFailure(domain, ses.BehaviorOnMXFailureRejectMessage), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsSESDomainMailFromExists("aws_ses_domain_mail_from.test"), + testAccCheckAwsSESDomainMailFromExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "behavior_on_mx_failure", ses.BehaviorOnMXFailureRejectMessage), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -65,12 +118,53 @@ func testAccCheckAwsSESDomainMailFromExists(n string) resource.TestCheckFunc { } } -const testAccAwsSESDomainMailFromConfig = ` +func testAccCheckSESDomainMailFromDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).sesConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ses_domain_mail_from" { + continue + } + + input := &ses.GetIdentityMailFromDomainAttributesInput{ + Identities: []*string{aws.String(rs.Primary.ID)}, + } + + out, err := conn.GetIdentityMailFromDomainAttributes(input) + if err != nil { + return fmt.Errorf("error fetching MAIL FROM domain attributes: %s", err) + } + if v, ok := out.MailFromDomainAttributes[rs.Primary.ID]; ok && v.MailFromDomain != nil && *v.MailFromDomain != "" { + return fmt.Errorf("MAIL FROM domain was not removed, found: %s", *v.MailFromDomain) + } + } + + return nil +} + +func testAccAwsSESDomainMailFromConfig(domain, mailFromDomain string) string { + return fmt.Sprintf(` resource "aws_ses_domain_identity" "test" { - domain = "%s" + domain = "%s" } + resource "aws_ses_domain_mail_from" "test" { - domain = "${aws_ses_domain_identity.test.domain}" - mail_from_domain = "bounce.${aws_ses_domain_identity.test.domain}" + domain = "${aws_ses_domain_identity.test.domain}" + mail_from_domain = "%s" +} +`, domain, mailFromDomain) +} + +func testAccAwsSESDomainMailFromConfig_behaviorOnMxFailure(domain, behaviorOnMxFailure string) string { + return fmt.Sprintf(` +resource "aws_ses_domain_identity" "test" { + domain = "%s" +} + +resource "aws_ses_domain_mail_from" "test" { + behavior_on_mx_failure = "%s" + domain = "${aws_ses_domain_identity.test.domain}" + mail_from_domain = "bounce.${aws_ses_domain_identity.test.domain}" +} +`, domain, behaviorOnMxFailure) } -` diff --git a/website/docs/r/ses_domain_mail_from.html.markdown b/website/docs/r/ses_domain_mail_from.html.markdown index 7fe2ea5deab..60124fd6ed9 100644 --- a/website/docs/r/ses_domain_mail_from.html.markdown +++ b/website/docs/r/ses_domain_mail_from.html.markdown @@ -6,58 +6,61 @@ description: |- Provides an SES domain MAIL FROM resource --- -# aws\_ses\_domain\_dkim +# aws_ses_domain_mail_from Provides an SES domain MAIL FROM resource. -Domain ownership needs to be confirmed first using [ses_domain_identity Resource](/docs/providers/aws/r/ses_domain_identity.html) - -## Argument Reference - -The following arguments are supported: - -* `domain` - (Required) Verified domain name to generate DKIM tokens for. -* `mail_from_domain` - (Required) Subdomain (of above domain) which is to be used as MAIL FROM address (Required for DMARC validation) - -## Followup Steps - -Find out more about setting MAIL FROM domain with Amazon SES in [docs](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/mail-from-set.html). - -* Create an MX record used to verify SES MAIL FROM setup - -* (Optionally) If you want your emails to pass SPF checks, you must publish an SPF record to the DNS server of the custom MAIL FROM domain. +~> **NOTE:** For the MAIL FROM domain to be fully usable, this resource should be paired with the [aws_ses_domain_identity resource](/docs/providers/aws/r/ses_domain_identity.html). To validate the MAIL FROM domain, a DNS MX record is required. To pass SPF checks, a DNS TXT record may also be required. See the [Amazon SES MAIL FROM documentation](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/mail-from-set.html) for more information. ## Example Usage ```hcl -resource "aws_ses_domain_identity" "example" { - domain = "example.com" +resource "aws_ses_domain_mail_from" "example" { + domain = "${aws_ses_domain_identity.example.domain}" + mail_from_domain = "bounce.${aws_ses_domain_identity.example.domain}" } -resource "aws_ses_domain_mail_from" "example" { - domain = "example.com" - mail_from_domain = "bounce.example.com" +# Example SES Domain Identity +resource "aws_ses_domain_identity" "example" { + domain = "example.com" } -resource "aws_route53_record" "example_amazonses_mail_from_mx_record" { - zone_id = "ABCDEFGHIJ123" # Change to appropriate Route53 Zone ID - name = "bounce.example.com" +# Example Route53 MX record +resource "aws_route53_record" "example_ses_domain_mail_from_mx" { + zone_id = "${aws_route53_zone.example.id}" + name = "${aws_ses_domain_mail_from.example.mail_from_domain}" type = "MX" ttl = "600" records = ["10 feedback-smtp.us-east-1.amazonses.com"] # Change to the region in which `aws_ses_domain_identity.example` is created } -# For SPF Check -resource "aws_route53_record" "example_amazonses_mail_from_mx_record" { - zone_id = "ABCDEFGHIJ123" # Change to appropriate Route53 Zone ID - name = "bounce.example.com" +# Example Route53 TXT record for SPF +resource "aws_route53_record" "example_ses_domain_mail_from_txt" { + zone_id = "${aws_route53_zone.example.id}" + name = "${aws_ses_domain_mail_from.example.mail_from_domain}" type = "TXT" ttl = "600" records = ["v=spf1 include:amazonses.com -all"] } - ``` +## Argument Reference + +The following arguments are required: + +* `domain` - (Required) Verified domain name to generate DKIM tokens for. +* `mail_from_domain` - (Required) Subdomain (of above domain) which is to be used as MAIL FROM address (Required for DMARC validation) + +The following arguments are optional: + +* `behavior_on_mx_failure` - (Optional) The action that you want Amazon SES to take if it cannot successfully read the required MX record when you send an email. Defaults to `UseDefaultValue`. See the [SES API documentation](https://docs.aws.amazon.com/ses/latest/APIReference/API_SetIdentityMailFromDomain.html) for more information. + +## Attributes Reference + +In addition to the arguments, which are exported, the following attributes are exported: + +* `id` - The domain name. + ## Import MAIL FROM domain can be imported using the `domain` attribute, e.g.