Plugins/ZoneEdit.ps1

function Get-CurrentPluginType { 'dns-01' }

function Add-DnsTxt {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [string]$RecordName,
        [Parameter(Mandatory,Position=1)]
        [string]$TxtValue,
        [Parameter(Mandatory)]
        [string]$ZEUsername,
        [Parameter(Mandatory)]
        [pscredential[]]$ZEDynCredential,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    # https://forum.zoneedit.com/threads/automating-changes-of-txt-records-in-dns.7394/page-2

    Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue"

    # get the cred that matches the record
    $dynCred = $ZEDynCredential | Where-Object {
        $RecordName -like "*$($_.UserName)"
    }
    if (-not $dynCred) {
        throw "ZEDynCredential did not contain any matches for $RecordName."
    }

    # create a new credential with the account username and zone specific password
    $cred = [pscredential]::new($ZEUsername,$dynCred.Password)

    $recHost = if ($dynCred.UserName -eq $RecordName) { "@.$RecordName" } else { $RecordName }

    # build the query
    $queryParams = @{
        Uri = 'https://dynamic.zoneedit.com/txt-create.php'
        Body = @{
            host = $recHost
            rdata = $TxtValue
        }
        Credential = $cred
        ErrorAction = 'Stop'
        Verbose = $false
    }

    try {
        Write-Debug "GET $($queryParams.Uri)`nhost = $recHost, rdata = $TxtValue"
        $response = Invoke-RestMethod @queryParams @script:UseBasic
    } catch { throw }

    # The response from the API seems to be XML, but it's not actually valid enough
    # for PowerShell's parser to auto-parse it. So we'll just grab what we care about
    # via Regex. Examples:
    # <SUCCESS CODE="200" TEXT="_acme-challenge.example.com TXT updated to test value" ZONE="example.com">
    # <SUCCESS CODE="200" TEXT="_acme-challenge.example.com TXT with rdata test value deleted" ZONE="example.com">
    # <ERROR CODE="702" PARAM="10" TEXT="Minimum 10 seconds between requests" ZONE="_acme-challenge.example.com">
    # <ERROR CODE="702" PARAM="10" TEXT="zone LOCKED" ZONE="_acme-challenge.example.com">
    # <ERROR CODE="703" TEXT="No rdata specified" ZONE="">

    # 702 error codes seem to mean, try again in at least 10 seconds either because the last
    # call was too recent or the zone is locked after the last update.
    while ($response -notlike '<SUCCESS*') {
        Write-Debug ($response.Trim())
        $null = $response -match '\<(SUCCESS|ERROR) CODE="(?<code>\d+)".*TEXT="(?<msg>[^"]+)"'

        if ($matches.code -eq '702') {

            # try again in 10 seconds
            Write-Verbose "Retrying in 10 seconds: $($matches.msg)"
            Start-Sleep -Seconds 10

            try {
                Write-Debug "GET $($queryParams.Uri)`nhost = $recHost, rdata = $TxtValue"
                $response = Invoke-RestMethod @queryParams @script:UseBasic
            } catch { throw }
            continue

        } else {
            # any other errors we've seen are permanent and don't need to be tried again
            throw "API Error: $($matches.msg)"
        }
    }

    <#
    .SYNOPSIS
        Add a DNS TXT record to ZoneEdit using their Dynamic TXT API.

    .DESCRIPTION
        Add a DNS TXT record to ZoneEdit using their Dynamic TXT API.

    .PARAMETER RecordName
        The fully qualified name of the TXT record.

    .PARAMETER TxtValue
        The value of the TXT record.

    .PARAMETER ZEUsername
        The username for your ZoneEdit account.

    .PARAMETER ZEDynCredential
        One or more PSCredential objects where the username is the zone name that
        contains the record you're updating and the password is the Dynamic Authentication
        Token configured for that zone.

    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.

    .EXAMPLE
        Add-DnsTxt '_acme-challenge.example.com' 'txt-value' -ZEUsername myuser -ZEDynCredential (Get-Credential)

        Adds a TXT record using the provided credentials.
    #>

}

function Remove-DnsTxt {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [string]$RecordName,
        [Parameter(Mandatory,Position=1)]
        [string]$TxtValue,
        [Parameter(Mandatory)]
        [string]$ZEUsername,
        [Parameter(Mandatory)]
        [pscredential[]]$ZEDynCredential,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    Write-Verbose "Removing a TXT record for $RecordName with value $TxtValue"

    # get the cred that matches the record
    $dynCred = $ZEDynCredential | Where-Object {
        $RecordName -like "*$($_.UserName)"
    }
    if (-not $dynCred) {
        throw "ZEDynCredential did not contain any matches for $RecordName."
    }

    # create a new credential with the account username and zone specific password
    $cred = [pscredential]::new($ZEUsername,$dynCred.Password)

    $recHost = if ($dynCred.UserName -eq $RecordName) { "@.$RecordName" } else { $RecordName }

    # build the query
    $queryParams = @{
        Uri = 'https://dynamic.zoneedit.com/txt-delete.php'
        Body = @{
            host = $recHost
            rdata = $TxtValue
        }
        Credential = $cred
        ErrorAction = 'Stop'
        Verbose = $false
    }

    try {
        Write-Debug "GET $($queryParams.Uri)`nhost = $recHost, rdata = $TxtValue"
        $response = Invoke-RestMethod @queryParams @script:UseBasic
    } catch { throw }

    # The response from the API seems to be XML, but it's not actually valid enough
    # for PowerShell's parser to auto-parse it. So we'll just grab what we care about
    # via Regex. Examples:
    # <SUCCESS CODE="200" TEXT="_acme-challenge.example.com TXT updated to test value" ZONE="example.com">
    # <SUCCESS CODE="200" TEXT="_acme-challenge.example.com TXT with rdata test value deleted" ZONE="example.com">
    # <ERROR CODE="702" PARAM="10" TEXT="Minimum 10 seconds between requests" ZONE="_acme-challenge.example.com">
    # <ERROR CODE="702" PARAM="10" TEXT="zone LOCKED" ZONE="_acme-challenge.example.com">
    # <ERROR CODE="703" TEXT="No rdata specified" ZONE="">

    # 702 error codes seem to mean, try again in at least 10 seconds either because the last
    # call was too recent or the zone is locked after the last update.
    while ($response -notlike '<SUCCESS*') {
        Write-Debug ($response.Trim())
        $null = $response -match '\<(SUCCESS|ERROR) CODE="(?<code>\d+)".*TEXT="(?<msg>[^"]+)"'

        if ($matches.code -eq '702') {

            # try again in 10 seconds
            Write-Verbose "Retrying in 10 seconds: $($matches.msg)"
            Start-Sleep -Seconds 10

            try {
                Write-Debug "GET $($queryParams.Uri)`nhost = $recHost, rdata = $TxtValue"
                $response = Invoke-RestMethod @queryParams @script:UseBasic
            } catch { throw }
            continue

        } else {
            # any other errors we've seen are permanent and don't need to be tried again
            throw "API Error: $($matches.msg)"
        }
    }

    <#
    .SYNOPSIS
        Add a DNS TXT record to ZoneEdit using their Dynamic TXT API.

    .DESCRIPTION
        Add a DNS TXT record to ZoneEdit using their Dynamic TXT API.

    .PARAMETER RecordName
        The fully qualified name of the TXT record.

    .PARAMETER TxtValue
        The value of the TXT record.

    .PARAMETER ZEUsername
        The username for your ZoneEdit account.

    .PARAMETER ZEDynCredential
        One or more PSCredential objects where the username is the zone name that
        contains the record you're updating and the password is the Dynamic Authentication
        Token configured for that zone.

    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.

    .EXAMPLE
        Add-DnsTxt '_acme-challenge.example.com' 'txt-value' -ZEUsername myuser -ZEDynCredential (Get-Credential)

        Adds a TXT record using the provided credentials.
    #>

}

function Save-DnsTxt {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )
    <#
    .SYNOPSIS
        Not required

    .DESCRIPTION
        This provider does not require calling this function to save DNS records.

    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.
    #>

}