diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2a876dbbed..44521088b6 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -41,6 +41,7 @@ jobs: - kafka - keymanager - lb + - mailbox - marketplace - mnq - mongodb diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index 7f17659c90..6dba191100 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -40,6 +40,7 @@ var foldersUsingVCRv4 = []string{ "jobs", "k8s", "keymanager", + "mailbox", "marketplace", "secret", } diff --git a/internal/services/mailbox/datasource_mailbox.go b/internal/services/mailbox/datasource_mailbox.go new file mode 100644 index 0000000000..378a672626 --- /dev/null +++ b/internal/services/mailbox/datasource_mailbox.go @@ -0,0 +1,86 @@ +package mailbox + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + mailboxsdk "github.com/scaleway/scaleway-sdk-go/api/mailbox/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/datasource" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" +) + +// DataSourceMailbox lets users look up a mailbox by its ID or by its email address. +func DataSourceMailbox() *schema.Resource { + dsSchema := datasource.SchemaFromResourceSchema(ResourceMailbox().SchemaFunc()) + + // All fields are computed from the resource schema; expose these lookup keys. + datasource.AddOptionalFieldsToSchema(dsSchema, "email") + + dsSchema["mailbox_id"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "UUID of the mailbox. Conflicts with email.", + ValidateDiagFunc: verify.IsUUID(), + ConflictsWith: []string{"email"}, + } + dsSchema["email"].ConflictsWith = []string{"mailbox_id"} + + return &schema.Resource{ + ReadContext: dataSourceMailboxRead, + Schema: dsSchema, + } +} + +func dataSourceMailboxRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + mailboxID, hasID := d.GetOk("mailbox_id") + + if hasID { + d.SetId(mailboxID.(string)) + return readMailboxIntoState(ctx, d, m) + } + + // Look up by email: list all mailboxes and filter. + email, hasEmail := d.GetOk("email") + if !hasEmail { + return diag.Errorf("one of mailbox_id or email must be provided") + } + + var foundID string + page := int32(1) + + for { + resp, err := api.ListMailboxes(&mailboxsdk.ListMailboxesRequest{ + Page: &page, + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + for _, mb := range resp.Mailboxes { + if mb.Email == email.(string) { + if foundID != "" { + return diag.Errorf("found multiple mailboxes with email %q", email) + } + foundID = mb.ID + } + } + + if uint64(int(page)*50) >= resp.TotalCount { + break + } + page++ + } + + if foundID == "" { + return diag.FromErr(fmt.Errorf("no mailbox found with email %q", email)) + } + + d.SetId(foundID) + + return readMailboxIntoState(ctx, d, m) +} diff --git a/internal/services/mailbox/datasource_mailbox_test.go b/internal/services/mailbox/datasource_mailbox_test.go new file mode 100644 index 0000000000..55dfabcbf1 --- /dev/null +++ b/internal/services/mailbox/datasource_mailbox_test.go @@ -0,0 +1,82 @@ +package mailbox_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" +) + +func TestAccDataSourceMailboxMailbox_ByID(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "ds_by_id" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "datasource.byid" + password = "S3cur3P@ssw0rd!" + subscription_period = "monthly" +} + +data "scaleway_mailbox_mailbox" "by_id" { + mailbox_id = scaleway_mailbox_mailbox.ds_by_id.id +} +`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair( + "data.scaleway_mailbox_mailbox.by_id", "id", + "scaleway_mailbox_mailbox.ds_by_id", "id", + ), + resource.TestCheckResourceAttrPair( + "data.scaleway_mailbox_mailbox.by_id", "email", + "scaleway_mailbox_mailbox.ds_by_id", "email", + ), + resource.TestCheckResourceAttrPair( + "data.scaleway_mailbox_mailbox.by_id", "status", + "scaleway_mailbox_mailbox.ds_by_id", "status", + ), + ), + }, + }, + }) +} + +func TestAccDataSourceMailboxMailbox_ByEmail(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "ds_by_email" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "datasource.byemail" + password = "S3cur3P@ssw0rd!" + subscription_period = "monthly" +} + +data "scaleway_mailbox_mailbox" "by_email" { + email = scaleway_mailbox_mailbox.ds_by_email.email +} +`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair( + "data.scaleway_mailbox_mailbox.by_email", "id", + "scaleway_mailbox_mailbox.ds_by_email", "id", + ), + resource.TestCheckResourceAttrPair( + "data.scaleway_mailbox_mailbox.by_email", "domain_id", + "scaleway_mailbox_mailbox.ds_by_email", "domain_id", + ), + ), + }, + }, + }) +} diff --git a/internal/services/mailbox/domain.go b/internal/services/mailbox/domain.go new file mode 100644 index 0000000000..b7fe214e6b --- /dev/null +++ b/internal/services/mailbox/domain.go @@ -0,0 +1,191 @@ +package mailbox + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + mailboxsdk "github.com/scaleway/scaleway-sdk-go/api/mailbox/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/identity" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/account" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" +) + +// ResourceDomain manages a mailbox service domain. +// A domain must be created before mailboxes can be provisioned under it. +// After creation, DNS records are available as computed attributes and must be +// configured in your DNS zone. The domain status will reflect validation progress. +func ResourceDomain() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceMailboxDomainCreate, + ReadContext: resourceMailboxDomainRead, + DeleteContext: resourceMailboxDomainDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(defaultDomainTimeout), + Delete: schema.DefaultTimeout(defaultDomainTimeout), + Default: schema.DefaultTimeout(defaultDomainTimeout), + }, + SchemaVersion: 0, + SchemaFunc: domainSchema, + Identity: identity.DefaultGlobal(), + } +} + +func domainSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Fully qualified domain name (e.g. mail.example.com)", + }, + "project_id": account.ProjectIDSchema(), + "status": { + Type: schema.TypeString, + Computed: true, + Description: "Domain status: creating, waiting_validation, validating, validation_failed, provisioning, ready, deleting", + }, + "mailbox_total_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of mailboxes provisioned on this domain", + }, + "webmail_url": { + Type: schema.TypeString, + Computed: true, + Description: "URL of the webmail interface", + }, + "imap_url": { + Type: schema.TypeString, + Computed: true, + Description: "IMAP server URL for email clients", + }, + "jmap_url": { + Type: schema.TypeString, + Computed: true, + Description: "JMAP server URL for email clients", + }, + "pop3_url": { + Type: schema.TypeString, + Computed: true, + Description: "POP3 server URL for email clients", + }, + "smtp_url": { + Type: schema.TypeString, + Computed: true, + Description: "SMTP server URL for email clients", + }, + "dns_records": { + Type: schema.TypeList, + Computed: true, + Description: "DNS records that must be configured in your DNS zone to validate the domain and enable mailbox features. Required records must be set before the domain can be used.", + Elem: dnsRecordSchema(), + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date and time of domain creation (RFC 3339 format)", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date and time of last update (RFC 3339 format)", + }, + } +} + +func resourceMailboxDomainCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + domain, err := api.CreateDomain(&mailboxsdk.CreateDomainRequest{ + ProjectID: d.Get("project_id").(string), + Name: d.Get("name").(string), + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + if err := identity.SetGlobalIdentity(d, domain.ID); err != nil { + return diag.FromErr(err) + } + + timeout := d.Timeout(schema.TimeoutCreate) + domain, err = api.WaitForDomain(&mailboxsdk.WaitForDomainRequest{ + DomainID: domain.ID, + Timeout: &timeout, + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + return setDomainState(ctx, d, api, domain) +} + +func resourceMailboxDomainRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + domain, err := api.GetDomain(&mailboxsdk.GetDomainRequest{DomainID: d.Id()}, scw.WithContext(ctx)) + if err != nil { + if httperrors.Is404(err) { + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + return setDomainState(ctx, d, api, domain) +} + +func resourceMailboxDomainDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + _, err := api.DeleteDomain(&mailboxsdk.DeleteDomainRequest{DomainID: d.Id()}, scw.WithContext(ctx)) + if err != nil { + if httperrors.Is404(err) { + return nil + } + return diag.FromErr(err) + } + + timeout := d.Timeout(schema.TimeoutDelete) + _, err = api.WaitForDomain(&mailboxsdk.WaitForDomainRequest{ + DomainID: d.Id(), + Timeout: &timeout, + }, scw.WithContext(ctx)) + if err != nil && !httperrors.Is404(err) { + return diag.FromErr(err) + } + + return nil +} + +// setDomainState writes all API-returned domain fields into the Terraform state. +func setDomainState(ctx context.Context, d *schema.ResourceData, api *mailboxsdk.API, domain *mailboxsdk.Domain) diag.Diagnostics { + _ = d.Set("name", domain.Name) + _ = d.Set("project_id", domain.ProjectID) + _ = d.Set("status", domain.Status.String()) + _ = d.Set("mailbox_total_count", int(domain.MailboxTotalCount)) + _ = d.Set("webmail_url", domain.WebmailURL) + _ = d.Set("imap_url", domain.ImapURL) + _ = d.Set("jmap_url", domain.JmapURL) + _ = d.Set("pop3_url", domain.Pop3URL) + _ = d.Set("smtp_url", domain.SMTPURL) + _ = d.Set("created_at", types.FlattenTime(domain.CreatedAt)) + _ = d.Set("updated_at", types.FlattenTime(domain.UpdatedAt)) + + records, err := api.GetDomainRecords(&mailboxsdk.GetDomainRecordsRequest{DomainID: domain.ID}, scw.WithContext(ctx)) + if err != nil && !httperrors.Is404(err) { + return diag.FromErr(err) + } + + if records != nil { + _ = d.Set("dns_records", flattenDNSRecords(records)) + } + + return nil +} diff --git a/internal/services/mailbox/domain_test.go b/internal/services/mailbox/domain_test.go new file mode 100644 index 0000000000..1a7b1c28c2 --- /dev/null +++ b/internal/services/mailbox/domain_test.go @@ -0,0 +1,99 @@ +package mailbox_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" + mailboxtestfuncs "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/mailbox/testfuncs" +) + +func TestAccMailboxDomain_Basic(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: mailboxtestfuncs.CheckDomainDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: ` + resource "scaleway_mailbox_domain" "basic" { + name = "terraform-test.example.com" + } + `, + Check: resource.ComposeTestCheckFunc( + mailboxtestfuncs.CheckDomainExists(tt, "scaleway_mailbox_domain.basic"), + resource.TestCheckResourceAttr("scaleway_mailbox_domain.basic", "name", "terraform-test.example.com"), + resource.TestCheckResourceAttrSet("scaleway_mailbox_domain.basic", "status"), + resource.TestCheckResourceAttrSet("scaleway_mailbox_domain.basic", "project_id"), + resource.TestCheckResourceAttrSet("scaleway_mailbox_domain.basic", "created_at"), + acctest.CheckResourceAttrUUID("scaleway_mailbox_domain.basic", "id"), + ), + }, + { + // Verify import by ID + ResourceName: "scaleway_mailbox_domain.basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccMailboxDomain_WithProjectID(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: mailboxtestfuncs.CheckDomainDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "scaleway_account_project" "mailbox_proj" { + name = "tf-mailbox-test" + } + + resource "scaleway_mailbox_domain" "with_project" { + name = "terraform-test.example.com" + project_id = scaleway_account_project.mailbox_proj.id + } + `), + Check: resource.ComposeTestCheckFunc( + mailboxtestfuncs.CheckDomainExists(tt, "scaleway_mailbox_domain.with_project"), + resource.TestCheckResourceAttrPair( + "scaleway_mailbox_domain.with_project", "project_id", + "scaleway_account_project.mailbox_proj", "id", + ), + resource.TestCheckResourceAttr("scaleway_mailbox_domain.with_project", "name", "terraform-test.example.com"), + ), + }, + }, + }) +} + +func TestAccMailboxDomain_DNSRecordsExposed(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: mailboxtestfuncs.CheckDomainDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: ` + resource "scaleway_mailbox_domain" "dns" { + name = "terraform-dns-test.example.com" + } + `, + Check: resource.ComposeTestCheckFunc( + mailboxtestfuncs.CheckDomainExists(tt, "scaleway_mailbox_domain.dns"), + // DNS records should be populated after domain creation + resource.TestCheckResourceAttrSet("scaleway_mailbox_domain.dns", "dns_records.#"), + ), + }, + }, + }) +} diff --git a/internal/services/mailbox/helpers.go b/internal/services/mailbox/helpers.go new file mode 100644 index 0000000000..776ffd9761 --- /dev/null +++ b/internal/services/mailbox/helpers.go @@ -0,0 +1,88 @@ +package mailbox + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + mailboxsdk "github.com/scaleway/scaleway-sdk-go/api/mailbox/v1alpha1" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" +) + +const ( + defaultDomainTimeout = 5 * time.Minute + defaultMailboxTimeout = 5 * time.Minute +) + +func newMailboxAPI(m any) *mailboxsdk.API { + return mailboxsdk.NewAPI(meta.ExtractScwClient(m)) +} + +// flattenDNSRecords converts a GetDomainRecordsResponse into a Terraform-compatible list. +func flattenDNSRecords(resp *mailboxsdk.GetDomainRecordsResponse) []any { + if resp == nil { + return nil + } + + records := []*mailboxsdk.DomainRecord{ + resp.Autoconfig, resp.Autodiscover, resp.Caldav, resp.Carddav, resp.Dkim, resp.Dmarc, + resp.DomainValidation, resp.Imap, resp.Jmap, resp.Mx, resp.Pop3, resp.Spf, resp.Submission, + } + + result := make([]any, 0) + + for _, rec := range records { + if rec == nil { + continue + } + m := map[string]any{ + "dns_type": rec.DNSType.String(), + "dns_name": rec.DNSName, + "dns_value": rec.DNSValue, + "status": rec.Status.String(), + "level": rec.Level.String(), + "error": types.FlattenStringPtr(rec.Error), + } + result = append(result, m) + } + + return result +} + +// dnsRecordSchema returns the schema for a single DNS record entry. +func dnsRecordSchema() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dns_type": { + Type: schema.TypeString, + Computed: true, + Description: "DNS record type (e.g. TXT, MX, CNAME)", + }, + "dns_name": { + Type: schema.TypeString, + Computed: true, + Description: "Fully qualified DNS name for this record", + }, + "dns_value": { + Type: schema.TypeString, + Computed: true, + Description: "DNS record value to set", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "Validation status of this record (valid, invalid, not_found, validating)", + }, + "level": { + Type: schema.TypeString, + Computed: true, + Description: "Requirement level (required, recommended, optional)", + }, + "error": { + Type: schema.TypeString, + Computed: true, + Description: "Error detail when the record is invalid", + }, + }, + } +} diff --git a/internal/services/mailbox/mailbox.go b/internal/services/mailbox/mailbox.go new file mode 100644 index 0000000000..406ddb2e49 --- /dev/null +++ b/internal/services/mailbox/mailbox.go @@ -0,0 +1,291 @@ +package mailbox + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + mailboxsdk "github.com/scaleway/scaleway-sdk-go/api/mailbox/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/identity" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" +) + +// ResourceMailbox manages a single mailbox on a Scaleway Mailbox domain. +// The mailbox is created via the batch endpoint (which supports single-item creation). +// Passwords are write-only: they are accepted on create/update but never read back from the API. +func ResourceMailbox() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceMailboxCreate, + ReadContext: resourceMailboxRead, + UpdateContext: resourceMailboxUpdate, + DeleteContext: resourceMailboxDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(defaultMailboxTimeout), + Update: schema.DefaultTimeout(defaultMailboxTimeout), + Delete: schema.DefaultTimeout(defaultMailboxTimeout), + Default: schema.DefaultTimeout(defaultMailboxTimeout), + }, + SchemaVersion: 0, + SchemaFunc: mailboxSchema, + Identity: identity.DefaultGlobal(), + } +} + +func mailboxSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "domain_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the mailbox domain to which this mailbox belongs", + ValidateDiagFunc: verify.IsUUID(), + }, + "local_part": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Local part of the email address (the part before the @). Changing this forces a new mailbox to be created.", + ValidateFunc: validation.StringLenBetween(1, 64), + }, + "password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Password for the mailbox. This value is write-only and will never be read back from the API. Changing this triggers an in-place update.", + }, + "subscription_period": { + Type: schema.TypeString, + Required: true, + Description: "Billing subscription period: monthly or yearly", + ValidateFunc: validation.StringInSlice([]string{ + string(mailboxsdk.MailboxSubscriptionPeriodMonthly), + string(mailboxsdk.MailboxSubscriptionPeriodYearly), + }, false), + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Display name shown in email clients (e.g. \"John Doe\")", + }, + "recovery_email": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Recovery email address used for password resets", + }, + "email": { + Type: schema.TypeString, + Computed: true, + Description: "Full email address of the mailbox (local_part@domain)", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "Mailbox status: creating, waiting_payment, waiting_domain, ready, deletion_scheduled, locked, renewing, deleting, restoring, payment_failed", + }, + "subscription_period_started_at": { + Type: schema.TypeString, + Computed: true, + Description: "Start date of the current subscription period (RFC 3339 format)", + }, + "next_subscription_period": { + Type: schema.TypeString, + Computed: true, + Description: "Next subscription renewal period (monthly, yearly, or canceled)", + }, + "next_subscription_period_starts_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date when the next subscription period starts (RFC 3339 format)", + }, + "deletion_scheduled_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of the unrecoverable mailbox deletion, set when status is deletion_scheduled (RFC 3339 format)", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date and time of mailbox creation (RFC 3339 format)", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date and time of last update (RFC 3339 format)", + }, + } +} + +func resourceMailboxCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + period := mailboxsdk.MailboxSubscriptionPeriod(d.Get("subscription_period").(string)) + + resp, err := api.BatchCreateMailboxes(&mailboxsdk.BatchCreateMailboxesRequest{ + DomainID: d.Get("domain_id").(string), + SubscriptionPeriod: period, + Mailboxes: []*mailboxsdk.BatchCreateMailboxesRequestMailboxParameters{ + { + LocalPart: d.Get("local_part").(string), + Password: d.Get("password").(string), + DisplayName: types.ExpandStringPtr(d.Get("display_name")), + RecoveryEmail: types.ExpandStringPtr(d.Get("recovery_email")), + }, + }, + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + if len(resp.Mailboxes) == 0 { + return diag.Errorf("mailbox creation returned no mailboxes") + } + + mb := resp.Mailboxes[0] + + if err := identity.SetGlobalIdentity(d, mb.ID); err != nil { + return diag.FromErr(err) + } + + timeout := d.Timeout(schema.TimeoutCreate) + mb, err = api.WaitForMailbox(&mailboxsdk.WaitForMailboxRequest{ + MailboxID: mb.ID, + Timeout: &timeout, + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + return setMailboxState(d, mb) +} + +func resourceMailboxRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + mb, err := api.GetMailbox(&mailboxsdk.GetMailboxRequest{MailboxID: d.Id()}, scw.WithContext(ctx)) + if err != nil { + if httperrors.Is404(err) { + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + return setMailboxState(d, mb) +} + +func resourceMailboxUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + req := &mailboxsdk.UpdateMailboxRequest{MailboxID: d.Id()} + needsUpdate := false + + if d.HasChange("display_name") { + req.DisplayName = types.ExpandStringPtr(d.Get("display_name")) + needsUpdate = true + } + + if d.HasChange("recovery_email") { + req.RecoveryEmail = types.ExpandStringPtr(d.Get("recovery_email")) + needsUpdate = true + } + + if d.HasChange("subscription_period") { + period := mailboxsdk.MailboxSubscriptionPeriod(d.Get("subscription_period").(string)) + req.SubscriptionPeriod = &period + needsUpdate = true + } + + if d.HasChange("password") { + req.NewPassword = types.ExpandStringPtr(d.Get("password")) + needsUpdate = true + } + + if !needsUpdate { + return resourceMailboxRead(ctx, d, m) + } + + mb, err := api.UpdateMailbox(req, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + timeout := d.Timeout(schema.TimeoutUpdate) + mb, err = api.WaitForMailbox(&mailboxsdk.WaitForMailboxRequest{ + MailboxID: mb.ID, + Timeout: &timeout, + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + return setMailboxState(d, mb) +} + +func resourceMailboxDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + _, err := api.DeleteMailbox(&mailboxsdk.DeleteMailboxRequest{MailboxID: d.Id()}, scw.WithContext(ctx)) + if err != nil { + if httperrors.Is404(err) { + return nil + } + return diag.FromErr(err) + } + + timeout := d.Timeout(schema.TimeoutDelete) + mb, err := api.WaitForMailbox(&mailboxsdk.WaitForMailboxRequest{ + MailboxID: d.Id(), + Timeout: &timeout, + }, scw.WithContext(ctx)) + if err != nil { + if httperrors.Is404(err) { + return nil + } + return diag.FromErr(err) + } + + if mb.Status == mailboxsdk.MailboxStatusDeletionScheduled { + return nil + } + + return nil +} + +// setMailboxState writes all API-returned mailbox fields into the Terraform state. +func setMailboxState(d *schema.ResourceData, mb *mailboxsdk.Mailbox) diag.Diagnostics { + _ = d.Set("domain_id", mb.DomainID) + _ = d.Set("email", mb.Email) + _ = d.Set("display_name", mb.DisplayName) + _ = d.Set("recovery_email", mb.RecoveryEmail) + _ = d.Set("status", mb.Status.String()) + _ = d.Set("subscription_period", mb.SubscriptionPeriod.String()) + _ = d.Set("subscription_period_started_at", types.FlattenTime(mb.SubscriptionPeriodStartedAt)) + _ = d.Set("next_subscription_period", mb.NextSubscriptionPeriod.String()) + _ = d.Set("next_subscription_period_starts_at", types.FlattenTime(mb.NextSubscriptionPeriodStartsAt)) + _ = d.Set("deletion_scheduled_at", types.FlattenTime(mb.DeletionScheduledAt)) + _ = d.Set("created_at", types.FlattenTime(mb.CreatedAt)) + _ = d.Set("updated_at", types.FlattenTime(mb.UpdatedAt)) + + return nil +} + +// readMailboxIntoState fetches the mailbox and writes it into d. +func readMailboxIntoState(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api := newMailboxAPI(m) + + mb, err := api.GetMailbox(&mailboxsdk.GetMailboxRequest{MailboxID: d.Id()}, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + return setMailboxState(d, mb) +} diff --git a/internal/services/mailbox/mailbox_test.go b/internal/services/mailbox/mailbox_test.go new file mode 100644 index 0000000000..1784f29440 --- /dev/null +++ b/internal/services/mailbox/mailbox_test.go @@ -0,0 +1,205 @@ +package mailbox_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" + mailboxtestfuncs "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/mailbox/testfuncs" +) + +const testDomainName = "terraform-test.example.com" + +// testConfigDomain returns a domain resource config reused across mailbox tests. +func testConfigDomain(name string) string { + return ` +resource "scaleway_mailbox_domain" "domain" { + name = "` + name + `" +} +` +} + +func TestAccMailboxMailbox_Basic(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: mailboxtestfuncs.CheckMailboxDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "basic" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "john.doe" + password = "S3cur3P@ssw0rd!" + subscription_period = "monthly" +} +`, + Check: resource.ComposeTestCheckFunc( + mailboxtestfuncs.CheckMailboxExists(tt, "scaleway_mailbox_mailbox.basic"), + resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.basic", "local_part", "john.doe"), + resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.basic", "subscription_period", "monthly"), + resource.TestCheckResourceAttrSet("scaleway_mailbox_mailbox.basic", "email"), + resource.TestCheckResourceAttrSet("scaleway_mailbox_mailbox.basic", "status"), + resource.TestCheckResourceAttrSet("scaleway_mailbox_mailbox.basic", "created_at"), + resource.TestCheckResourceAttrPair( + "scaleway_mailbox_mailbox.basic", "domain_id", + "scaleway_mailbox_domain.domain", "id", + ), + acctest.CheckResourceAttrUUID("scaleway_mailbox_mailbox.basic", "id"), + ), + }, + { + // Verify import by ID + ResourceName: "scaleway_mailbox_mailbox.basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, + }, + }) +} + +func TestAccMailboxMailbox_WithOptionalFields(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: mailboxtestfuncs.CheckMailboxDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "full" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "jane.doe" + password = "S3cur3P@ssw0rd!" + display_name = "Jane Doe" + recovery_email = "recovery@external.example.com" + subscription_period = "monthly" +} +`, + Check: resource.ComposeTestCheckFunc( + mailboxtestfuncs.CheckMailboxExists(tt, "scaleway_mailbox_mailbox.full"), + resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.full", "display_name", "Jane Doe"), + resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.full", "recovery_email", "recovery@external.example.com"), + ), + }, + }, + }) +} + +func TestAccMailboxMailbox_Update(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: mailboxtestfuncs.CheckMailboxDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "update" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "update.test" + password = "S3cur3P@ssw0rd!" + display_name = "Update Test" + subscription_period = "monthly" +} +`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.update", "display_name", "Update Test"), + resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.update", "subscription_period", "monthly"), + ), + }, + { + // Update display_name and recovery_email in-place + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "update" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "update.test" + password = "S3cur3P@ssw0rd!" + display_name = "Updated Name" + recovery_email = "new-recovery@external.example.com" + subscription_period = "monthly" +} +`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.update", "display_name", "Updated Name"), + resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.update", "recovery_email", "new-recovery@external.example.com"), + ), + }, + }, + }) +} + +func TestAccMailboxMailbox_PasswordChange(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: mailboxtestfuncs.CheckMailboxDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "pwd" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "pwd.change" + password = "S3cur3P@ssw0rd!" + subscription_period = "monthly" +} +`, + Check: mailboxtestfuncs.CheckMailboxExists(tt, "scaleway_mailbox_mailbox.pwd"), + }, + { + // Password change should trigger an in-place update, not a recreation + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "pwd" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "pwd.change" + password = "N3wS3cur3P@ssw0rd!" + subscription_period = "monthly" +} +`, + Check: mailboxtestfuncs.CheckMailboxExists(tt, "scaleway_mailbox_mailbox.pwd"), + }, + }, + }) +} + +func TestAccMailboxMailbox_ForceNewOnLocalPartChange(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + CheckDestroy: mailboxtestfuncs.CheckMailboxDestroyed(tt), + Steps: []resource.TestStep{ + { + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "force_new" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "original" + password = "S3cur3P@ssw0rd!" + subscription_period = "monthly" +} +`, + Check: resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.force_new", "local_part", "original"), + }, + { + // Changing local_part must force a new resource + Config: testConfigDomain(testDomainName) + ` +resource "scaleway_mailbox_mailbox" "force_new" { + domain_id = scaleway_mailbox_domain.domain.id + local_part = "renamed" + password = "S3cur3P@ssw0rd!" + subscription_period = "monthly" +} +`, + Check: resource.TestCheckResourceAttr("scaleway_mailbox_mailbox.force_new", "local_part", "renamed"), + }, + }, + }) +} diff --git a/internal/services/mailbox/sweep_test.go b/internal/services/mailbox/sweep_test.go new file mode 100644 index 0000000000..121826c494 --- /dev/null +++ b/internal/services/mailbox/sweep_test.go @@ -0,0 +1,16 @@ +package mailbox_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + mailboxtestfuncs "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/mailbox/testfuncs" +) + +func init() { + mailboxtestfuncs.AddTestSweepers() +} + +func TestMain(m *testing.M) { + resource.TestMain(m) +} diff --git a/internal/services/mailbox/testfuncs/checks.go b/internal/services/mailbox/testfuncs/checks.go new file mode 100644 index 0000000000..4700eb0f28 --- /dev/null +++ b/internal/services/mailbox/testfuncs/checks.go @@ -0,0 +1,97 @@ +package mailboxtestfuncs + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + mailboxsdk "github.com/scaleway/scaleway-sdk-go/api/mailbox/v1alpha1" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors" +) + +// CheckMailboxDestroyed verifies that all mailbox resources in state have been deleted. +func CheckMailboxDestroyed(tt *acctest.TestTools) resource.TestCheckFunc { + return func(state *terraform.State) error { + api := mailboxsdk.NewAPI(tt.Meta.ScwClient()) + + for _, rs := range state.RootModule().Resources { + if rs.Type != "scaleway_mailbox_mailbox" { + continue + } + + _, err := api.GetMailbox(&mailboxsdk.GetMailboxRequest{MailboxID: rs.Primary.ID}) + if err == nil { + return fmt.Errorf("mailbox %s still exists", rs.Primary.ID) + } + + if !httperrors.Is404(err) { + return fmt.Errorf("unexpected error checking mailbox %s: %w", rs.Primary.ID, err) + } + } + + return nil + } +} + +// CheckDomainDestroyed verifies that all mailbox domain resources in state have been deleted. +func CheckDomainDestroyed(tt *acctest.TestTools) resource.TestCheckFunc { + return func(state *terraform.State) error { + api := mailboxsdk.NewAPI(tt.Meta.ScwClient()) + + for _, rs := range state.RootModule().Resources { + if rs.Type != "scaleway_mailbox_domain" { + continue + } + + _, err := api.GetDomain(&mailboxsdk.GetDomainRequest{DomainID: rs.Primary.ID}) + if err == nil { + return fmt.Errorf("mailbox domain %s still exists", rs.Primary.ID) + } + + if !httperrors.Is404(err) { + return fmt.Errorf("unexpected error checking domain %s: %w", rs.Primary.ID, err) + } + } + + return nil + } +} + +// CheckMailboxExists verifies a mailbox resource exists in both state and API. +func CheckMailboxExists(tt *acctest.TestTools, n string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[n] + if !ok { + return fmt.Errorf("resource %q not found in state", n) + } + + api := mailboxsdk.NewAPI(tt.Meta.ScwClient()) + + _, err := api.GetMailbox(&mailboxsdk.GetMailboxRequest{MailboxID: rs.Primary.ID}) + if err != nil { + return fmt.Errorf("error reading mailbox %s: %w", rs.Primary.ID, err) + } + + return nil + } +} + +// CheckDomainExists verifies a domain resource exists in both state and API. +func CheckDomainExists(tt *acctest.TestTools, n string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[n] + if !ok { + return fmt.Errorf("resource %q not found in state", n) + } + + api := mailboxsdk.NewAPI(tt.Meta.ScwClient()) + + _, err := api.GetDomain(&mailboxsdk.GetDomainRequest{DomainID: rs.Primary.ID}) + if err != nil { + return fmt.Errorf("error reading domain %s: %w", rs.Primary.ID, err) + } + + return nil + } +} diff --git a/internal/services/mailbox/testfuncs/sweep.go b/internal/services/mailbox/testfuncs/sweep.go new file mode 100644 index 0000000000..552b8945b2 --- /dev/null +++ b/internal/services/mailbox/testfuncs/sweep.go @@ -0,0 +1,69 @@ +package mailboxtestfuncs + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + mailboxsdk "github.com/scaleway/scaleway-sdk-go/api/mailbox/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/logging" +) + +// AddTestSweepers registers sweepers that clean up test resources. +func AddTestSweepers() { + resource.AddTestSweepers("scaleway_mailbox_mailbox", &resource.Sweeper{ + Name: "scaleway_mailbox_mailbox", + F: testSweepMailboxes, + }) + + resource.AddTestSweepers("scaleway_mailbox_domain", &resource.Sweeper{ + Name: "scaleway_mailbox_domain", + F: testSweepDomains, + Dependencies: []string{"scaleway_mailbox_mailbox"}, + }) +} + +func testSweepMailboxes(_ string) error { + return acctest.Sweep(func(scwClient *scw.Client) error { + api := mailboxsdk.NewAPI(scwClient) + + logging.L.Debugf("sweeper: deleting test mailboxes") + + resp, err := api.ListMailboxes(&mailboxsdk.ListMailboxesRequest{}, scw.WithAllPages()) + if err != nil { + return fmt.Errorf("error listing mailboxes in sweeper: %w", err) + } + + for _, mb := range resp.Mailboxes { + _, err := api.DeleteMailbox(&mailboxsdk.DeleteMailboxRequest{MailboxID: mb.ID}) + if err != nil { + logging.L.Debugf("sweeper: error deleting mailbox %s: %s", mb.ID, err) + } + } + + return nil + }) +} + +func testSweepDomains(_ string) error { + return acctest.Sweep(func(scwClient *scw.Client) error { + api := mailboxsdk.NewAPI(scwClient) + + logging.L.Debugf("sweeper: deleting test mailbox domains") + + resp, err := api.ListDomains(&mailboxsdk.ListDomainsRequest{}, scw.WithAllPages()) + if err != nil { + return fmt.Errorf("error listing mailbox domains in sweeper: %w", err) + } + + for _, domain := range resp.Domains { + _, err := api.DeleteDomain(&mailboxsdk.DeleteDomainRequest{DomainID: domain.ID}) + if err != nil { + logging.L.Debugf("sweeper: error deleting domain %s: %s", domain.ID, err) + } + } + + return nil + }) +} diff --git a/provider/sdkv2.go b/provider/sdkv2.go index f90a1df9b8..1b7e4884cd 100644 --- a/provider/sdkv2.go +++ b/provider/sdkv2.go @@ -39,6 +39,7 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/kafka" "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/keymanager" "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/lb" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/mailbox" "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/marketplace" "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/mnq" "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/mongodb" @@ -199,6 +200,8 @@ func SDKProvider(config *Config) plugin.ProviderFunc { "scaleway_iot_hub": iot.ResourceHub(), "scaleway_iot_network": iot.ResourceNetwork(), "scaleway_iot_route": iot.ResourceRoute(), + "scaleway_mailbox_domain": mailbox.ResourceDomain(), + "scaleway_mailbox_mailbox": mailbox.ResourceMailbox(), "scaleway_ipam_ip": ipam.ResourceIP(), "scaleway_ipam_ip_reverse_dns": ipam.ResourceIPReverseDNS(), "scaleway_job_definition": jobs.ResourceDefinition(), @@ -335,6 +338,7 @@ func SDKProvider(config *Config) plugin.ProviderFunc { "scaleway_interlink_pops": interlink.DataSourcePops(), "scaleway_iot_device": iot.DataSourceDevice(), "scaleway_iot_hub": iot.DataSourceHub(), + "scaleway_mailbox_mailbox": mailbox.DataSourceMailbox(), "scaleway_ipam_ip": ipam.DataSourceIP(), "scaleway_ipam_ips": ipam.DataSourceIPs(), "scaleway_kafka_cluster": kafka.DataSourceCluster(), diff --git a/templates/data-sources/mailbox_mailbox.md.tmpl b/templates/data-sources/mailbox_mailbox.md.tmpl new file mode 100644 index 0000000000..8b6a75d251 --- /dev/null +++ b/templates/data-sources/mailbox_mailbox.md.tmpl @@ -0,0 +1,54 @@ +--- +subcategory: "Mailbox" +page_title: "Scaleway: scaleway_mailbox_mailbox" +--- + +# scaleway_mailbox_mailbox + +Use this data source to get information about a Scaleway Mailbox mailbox based on its UUID +or its full email address. + +~> **Note** This data source is in **alpha** and the API is subject to change. + +## Example Usage + +```hcl +# Look up by mailbox UUID +data "scaleway_mailbox_mailbox" "by_id" { + mailbox_id = "11111111-1111-1111-1111-111111111111" +} + +# Look up by email address +data "scaleway_mailbox_mailbox" "by_email" { + email = "john.doe@mail.example.com" +} + +output "mailbox_status" { + value = data.scaleway_mailbox_mailbox.by_email.status +} +``` + +## Argument Reference + +- `mailbox_id` - (Optional) UUID of the mailbox. Conflicts with `email`. +- `email` - (Optional) Full email address of the mailbox (`local_part@domain`). Conflicts with `mailbox_id`. + +-> **Note** You must specify exactly one of `mailbox_id` or `email`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +- `id` - Unique identifier of the mailbox (UUID). +- `domain_id` - UUID of the domain to which this mailbox belongs. +- `email` - Full email address (`local_part@domain`). +- `display_name` - Display name of the mailbox. +- `recovery_email` - Recovery email address. +- `status` - Current mailbox status (`ready`, `locked`, `deletion_scheduled`, etc.). +- `subscription_period` - Active billing period (`monthly` or `yearly`). +- `subscription_period_started_at` - Start date of the current billing period (RFC 3339 format). +- `next_subscription_period` - Next renewal period. +- `next_subscription_period_starts_at` - Date when the next period starts (RFC 3339 format). +- `deletion_scheduled_at` - Scheduled deletion date if status is `deletion_scheduled` (RFC 3339 format). +- `created_at` - Date and time of mailbox creation (RFC 3339 format). +- `updated_at` - Date and time of last update (RFC 3339 format). diff --git a/templates/resources/mailbox_domain.md.tmpl b/templates/resources/mailbox_domain.md.tmpl new file mode 100644 index 0000000000..3f1c560650 --- /dev/null +++ b/templates/resources/mailbox_domain.md.tmpl @@ -0,0 +1,108 @@ +{{- /*gotype: github.com/hashicorp/terraform-plugin-docs/internal/provider.ResourceTemplateType */ -}} +--- +subcategory: "Mailbox" +page_title: "Scaleway: scaleway_mailbox_domain" +--- + +# Resource: scaleway_mailbox_domain + +Creates and manages a [Scaleway Mailbox](https://www.scaleway.com/en/developers/api/mailbox/) domain. + +A **domain** is the prerequisite for creating mailboxes. After creation the resource exposes +all required DNS records in the `dns_records` attribute so you can delegate them to your DNS +provider. The domain `status` reflects validation progress. + +~> **Note** This resource is in **alpha** and the API is subject to change. + +## Example Usage + +### Minimal domain + +```terraform +resource "scaleway_mailbox_domain" "example" { + name = "mail.example.com" +} +``` + +### Domain with DNS records configured via Scaleway DNS + +```terraform +variable "domain_name" { + type = string +} + +resource "scaleway_mailbox_domain" "main" { + name = var.domain_name +} + +# Iterate over all required DNS records and create them automatically. +# The dns_records list contains every record (MX, SPF, DKIM, DMARC, …) that +# must be set before the domain can be validated. +resource "scaleway_domain_record" "mailbox_dns" { + for_each = { + for rec in scaleway_mailbox_domain.main.dns_records : + "${rec.dns_type}-${rec.dns_name}" => rec + if rec.level == "required" + } + + dns_zone = var.domain_name + name = each.value.dns_name + type = each.value.dns_type + data = each.value.dns_value +} +``` + +### Domain in a specific project + +```terraform +resource "scaleway_account_project" "mail_project" { + name = "mail-project" +} + +resource "scaleway_mailbox_domain" "project_domain" { + name = "mail.example.com" + project_id = scaleway_account_project.mail_project.id +} +``` + +## Argument Reference + +- `name` - (Required, Forces new resource) Fully qualified domain name (e.g. `mail.example.com`). +- `project_id` - (Optional, Defaults to [provider](../index.md#project_id) `project_id`) The project ID owning the domain. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +- `id` - Unique identifier of the domain (UUID). +- `status` - Current status of the domain: + - `creating` – domain is being registered. + - `waiting_validation` – DNS records must be configured and validated. + - `validating` – validation is in progress. + - `validation_failed` – validation failed; check `dns_records[*].error`. + - `provisioning` – domain is being provisioned. + - `ready` – domain is ready and mailboxes can be created. + - `deleting` – domain is being deleted. +- `mailbox_total_count` - Number of mailboxes currently provisioned on this domain. +- `webmail_url` - URL of the domain's webmail interface. +- `imap_url` - IMAP server URL for configuring email clients. +- `jmap_url` - JMAP server URL for configuring email clients. +- `pop3_url` - POP3 server URL for configuring email clients. +- `smtp_url` - SMTP server URL for configuring email clients. +- `dns_records` - List of DNS records to configure in your DNS zone. Each entry has: + - `dns_type` – Record type (TXT, MX, CNAME, SRV…). + - `dns_name` – Fully qualified name for this record. + - `dns_value` – Value to set for this record. + - `status` – Validation status (`valid`, `invalid`, `not_found`, `validating`). + - `level` – Requirement level (`required`, `recommended`, `optional`). + - `error` – Error detail when the record is invalid or not found. +- `created_at` - Date and time of domain creation (RFC 3339 format). +- `updated_at` - Date and time of last update (RFC 3339 format). + +## Import + +Mailbox domains can be imported using their UUID: + +```bash +terraform import scaleway_mailbox_domain.example 11111111-1111-1111-1111-111111111111 +``` diff --git a/templates/resources/mailbox_mailbox.md.tmpl b/templates/resources/mailbox_mailbox.md.tmpl new file mode 100644 index 0000000000..7d359f4ab6 --- /dev/null +++ b/templates/resources/mailbox_mailbox.md.tmpl @@ -0,0 +1,120 @@ +{{- /*gotype: github.com/hashicorp/terraform-plugin-docs/internal/provider.ResourceTemplateType */ -}} +--- +subcategory: "Mailbox" +page_title: "Scaleway: scaleway_mailbox_mailbox" +--- + +# Resource: scaleway_mailbox_mailbox + +Creates and manages a [Scaleway Mailbox](https://www.scaleway.com/en/developers/api/mailbox/) mailbox. + +A **mailbox** is a hosted email address (`local_part@domain`) that belongs to a +`scaleway_mailbox_domain`. The password is write-only: it is accepted on creation and +update but is never read back from the API and will not appear in plan output or state files. + +~> **Note** This resource is in **alpha** and the API is subject to change. + +## Example Usage + +### Basic mailbox + +```terraform +resource "scaleway_mailbox_domain" "main" { + name = "mail.example.com" +} + +resource "scaleway_mailbox_mailbox" "john" { + domain_id = scaleway_mailbox_domain.main.id + local_part = "john.doe" + password = var.mailbox_password + subscription_period = "monthly" +} +``` + +### Mailbox with display name and recovery email + +```terraform +resource "scaleway_mailbox_domain" "main" { + name = "mail.example.com" +} + +resource "scaleway_mailbox_mailbox" "support" { + domain_id = scaleway_mailbox_domain.main.id + local_part = "support" + password = var.mailbox_password + display_name = "Support Team" + recovery_email = "admin@other-domain.com" + subscription_period = "yearly" +} + +output "support_email" { + value = scaleway_mailbox_mailbox.support.email +} +``` + +### Multiple mailboxes on one domain + +```terraform +resource "scaleway_mailbox_domain" "corp" { + name = "corp.example.com" +} + +locals { + mailboxes = { + alice = { display_name = "Alice" } + bob = { display_name = "Bob" } + } +} + +resource "scaleway_mailbox_mailbox" "employees" { + for_each = local.mailboxes + + domain_id = scaleway_mailbox_domain.corp.id + local_part = each.key + password = var.mailbox_password + display_name = each.value.display_name + subscription_period = "monthly" +} +``` + +## Argument Reference + +- `domain_id` - (Required, Forces new resource) UUID of the `scaleway_mailbox_domain` to which this mailbox belongs. +- `local_part` - (Required, Forces new resource) Local part of the email address (part before the `@`). Must be 1–64 characters. Changing this value forces a new mailbox to be created. +- `password` - (Required, **Sensitive**, Write-only) Password for the mailbox. This value is never returned by the API. Changing it triggers an in-place update. +- `subscription_period` - (Required) Billing period for the mailbox. Accepted values: `monthly`, `yearly`. +- `display_name` - (Optional) Display name shown in email clients (e.g. `"John Doe"`). +- `recovery_email` - (Optional) External email address used for account recovery. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +- `id` - Unique identifier of the mailbox (UUID). +- `email` - Full email address of the mailbox (`local_part@domain`). +- `status` - Current status of the mailbox: + - `creating` – mailbox is being provisioned. + - `waiting_payment` – awaiting payment confirmation. + - `waiting_domain` – domain is not yet ready. + - `ready` – mailbox is active and ready to use. + - `deletion_scheduled` – mailbox is scheduled for deletion. + - `locked` – mailbox is locked (e.g. billing issue). + - `renewing` – subscription renewal is in progress. + - `deleting` – mailbox is being deleted. + - `restoring` – mailbox is being restored. + - `payment_failed` – the latest payment failed. +- `subscription_period_started_at` - Start date of the current billing period (RFC 3339 format). +- `next_subscription_period` - Next renewal period (`monthly`, `yearly`, or `canceled`). +- `next_subscription_period_starts_at` - Date when the next billing period begins (RFC 3339 format). +- `deletion_scheduled_at` - Scheduled deletion date when status is `deletion_scheduled` (RFC 3339 format). +- `created_at` - Date and time of mailbox creation (RFC 3339 format). +- `updated_at` - Date and time of last update (RFC 3339 format). + +## Import + +Mailboxes can be imported using their UUID. The `password` field cannot be recovered and will +show as an empty diff after import. + +```bash +terraform import scaleway_mailbox_mailbox.john 11111111-1111-1111-1111-111111111111 +```