plugins/emarsys/Private/classes/emarsys.ps1

<#
# https://www.coolgenerator.com/ascii-text-generator
#>


################################################
#
# GENERIC CLASSES AND ENUMS
#
################################################


#-----------------------------------------------
# SCOPE
#-----------------------------------------------

enum DSCPScope {
    Global = 0          # global
    Local = 1           # list/local
    Transactional = 2   # Transactional
}


#-----------------------------------------------
# DIGITAL CHANNEL SERVICE PROVIDER
#-----------------------------------------------

class DCSP {

    [String]$providerName
    static [bool]$allowNewFieldCreation = $false

}


#-----------------------------------------------
# FIELDS
#-----------------------------------------------

class DCSPField {

    [String] $id
    [String] $name
    [String] $label
    [String] $description
    [String] $placeholder
    [String] $dataType
    [String[]] $synonyms
    [DSCPScope] $scope = [DSCPScope]::Global
    [bool] $required = $false
    [String[]] $dependency # Dependency to other field
    [DCSPFieldChoice[]] $choices  # For selector values

    # empty default constructor needed to support hashtable constructor
    DCSPField () {
    }

}

class DCSPFieldChoice {

    [String] $id
    [String] $label
    [String] $description

    # empty default constructor needed to support hashtable constructor
    DCSPFieldChoice () {
    }

}


#-----------------------------------------------
# LISTS
#-----------------------------------------------

# TODO [ ] think again about this class

class DCSPList {

    #-----------------------------------------------
    # PROPERTIES (can be public by default, static or hidden)
    #-----------------------------------------------

    [String]$id
    [String]$name = ""
    [DateTime]$created
    [DateTime]$updated

    hidden [String] $nameConcatChar = " / "
    #hidden [String]$type = " / "


    #-----------------------------------------------
    # PUBLIC CONSTRUCTORS
    #-----------------------------------------------

    # empty default constructor needed to support hashtable constructor
    DCSPList () {

        $this.init()

    }


    DCSPList ( [String]$inputString ) {

        # If we have a nameconcat char in the settings variable, just use it
        $this.init($inputString)

    }


    #-----------------------------------------------
    # HIDDEN CONSTRUCTORS - CHAINING
    #-----------------------------------------------


    [void] init () {
        # If we have a nameconcat char in the settings variable, just use it
        if ( $script:settings.nameConcatChar ) {
            $this.nameConcatChar = $script:settings.nameConcatChar
        }
    }

    # Used for a minimal input
    [void] init ([String]$inputString ) {

        $this.init()

        $stringParts = $inputString -split [regex]::Escape($this.nameConcatChar.trim()),2
        $this.id = $stringParts[0].trim()
        $this.name = $stringParts[1].trim()

    }


    #-----------------------------------------------
    # METHODS
    #-----------------------------------------------

    [String] toString()
    {
        return $this.id, $this.name -join $this.nameConcatChar
    }

}


#-----------------------------------------------
# MAILINGS - GENERIC
#-----------------------------------------------

# TODO [ ] think again about this class

class DCSPMailing {

    #-----------------------------------------------
    # PROPERTIES (can be public by default, static or hidden)
    #-----------------------------------------------

    [String]$id
    [String]$name = ""
    [DateTime]$created

    hidden [String] $nameConcatChar = " / "
    #hidden [String]$type = " / "


    #-----------------------------------------------
    # PUBLIC CONSTRUCTORS
    #-----------------------------------------------

    # empty default constructor needed to support hashtable constructor
    DCSPMailing () {

        $this.init()

    }


    DCSPMailing ( [String]$inputString ) {

        # If we have a nameconcat char in the settings variable, just use it
        $this.init($inputString)

    }


    #-----------------------------------------------
    # HIDDEN CONSTRUCTORS - CHAINING
    #-----------------------------------------------


    [void] init () {
        # If we have a nameconcat char in the settings variable, just use it
        if ( $script:settings.nameConcatChar ) {
            $this.nameConcatChar = $script:settings.nameConcatChar
        }
    }

    # Used for a minimal input
    [void] init ([String]$inputString ) {

        $this.init()

        $stringParts = $inputString -split [regex]::Escape($this.nameConcatChar.trim()),2
        $this.id = $stringParts[0].trim()
        $this.name = $stringParts[1].trim()

    }


    #-----------------------------------------------
    # METHODS
    #-----------------------------------------------

    [String] toString()
    {
        return $this.id, $this.name -join $this.nameConcatChar
    }

}


#-----------------------------------------------
# MAILINGS - EMAIL
#-----------------------------------------------

enum DCSPMailingsEmailContentTypes {
    html = 10
    text = 20
    block = 30
}

# Additional properties for email channel
class DCSPMailingsEmail : DCSPMailing {

    #-----------------------------------------------
    # PROPERTIES (can be public by default, static or hidden)
    #-----------------------------------------------

    [String]$subject
    [String]$fromEmail
    [String]$fromName
    [DCSPMailingsEmailContentTypes]$contentType

}


################################################
#
# INHERITED CLASSES AND ENUMS
#
################################################


#-----------------------------------------------
# EMARSYS FIELDS
#-----------------------------------------------

enum EmarsysFieldApplicationTypes {
    shorttext = 0
    longtext = 1
    largetext = 2
    date = 3
    url = 4
    numeric = 5
}

# TODO [ ] implement language code for fields

class EmarsysField : DCSPField {

    hidden [Emarsys]$emarsys
    [bool]$excludeForExport = $false

    EmarsysField () {
        if ( $_.id -in @(27, 28, 29, 32, 33) ) {
            $this.excludeForExport = $true
        }
    }

    delete() {

        # TODO [ ] check if right

        # Call emarsys
        $params = @{
            cred = $this.emarsys.cred
            uri = "$( $this.emarsys.baseUrl)field/$( $this.id )"
            method = "Delete"
        }
        $res = Invoke-emarsys @params

    }

}


#-----------------------------------------------
# EMARSYS LISTS
#-----------------------------------------------

class EmarsysList : DCSPList {

    #-----------------------------------------------
    # PROPERTIES (can be public by default, static or hidden)
    #-----------------------------------------------

    [int] $type
    hidden [Emarsys]$emarsys
    [PSCustomObject]$raw        # the raw source object for this one


    #-----------------------------------------------
    # PUBLIC CONSTRUCTORS
    #-----------------------------------------------

    # empty default constructor needed to support hashtable constructor
    EmarsysList () {

        # TODO [ ] needed?
        #$this.init()

    }

    #-----------------------------------------------
    # METHODS
    #-----------------------------------------------

    # Returns the number of contacts in a contact list.
    [String] count() {

        # Call emarsys
        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl)contactlist/$( $this.id )/count"
        }
        [int]$res = Invoke-emarsys @params

        return [int]$res
    }

}

class EmarsysMailing : DCSPMailingsEmail {


    #-----------------------------------------------
    # PROPERTIES (can be public by default, static or hidden)
    #-----------------------------------------------

    hidden [Emarsys]$emarsys
    [PSCustomObject]$raw        # the raw source object for this one
    [String]$language


    #-----------------------------------------------
    # PUBLIC CONSTRUCTORS
    #-----------------------------------------------

    # empty default constructor needed to support hashtable constructor
    EmarsysMailing () {

        # TODO [ ] needed?
        #$this.init()

    }

    #-----------------------------------------------
    # METHODS
    #-----------------------------------------------
    <#
    getDetails() {
 
        # Details
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)email/$( $this.id )"
        }
        Invoke-emarsys @params | select * -ExcludeProperty "html_source","text_source" | Out-GridView
    }
    #>


    [PSCustomObject] getResponseSummary() {

        # Response summary
        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl)email/$( $this.id )/responsesummary" # ?launch_id={{launch_id}}&start_date={{start_date}}&end_date={{end_date}}
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getLaunches() {

        $body = @{
            emailId = $this.id # html|text|mobile
        }
        $bodyJson = ConvertTo-Json -InputObject $body -Depth 20

        # Call emarsys
        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl)email/getlaunchesofemail"
            method = "Post"
            body = $bodyJson
            verbose = $true
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getDeliveryStatus() {
        return getDeliveryStatus(0)
    }

    [PSCustomObject] getDeliveryStatus([int]$launchId) {

        # https://dev.emarsys.com/v2/email-campaign-life-cycle/query-delivery-status

        $body = @{
            emailId = $this.id
            #lastId
            #allowNotFinished
        }

        # Add launch id if not zero
        if ( $launchId -gt 0 ) {
            $body | Add-Member -MemberType NoteProperty -Name "launchId" -Value $launchId
        }

        $bodyJson = ConvertTo-Json -InputObject $body -Depth 20

        # Call emarsys
        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl)email/getdeliverystatus"
            method = "Post"
            body = $bodyJson
            verbose = $true
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getPreview() {

        return $this.getPreview("html")

    }

    # put in html, text, mobile
    [PSCustomObject] getPreview([String]$version) {

        # https://dev.emarsys.com/v2/email-campaign-life-cycle/preview-email-campaign-contents

        $body = @{
            version = $version # html|text|mobile
        }
        $bodyJson = ConvertTo-Json -InputObject $body -Depth 20

        # Call emarsys
        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl)email/$( $this.id )/preview"
            method = "Post"
            body = $bodyJson
            verbose = $true
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] sendTest([String]$subject, [EmarsysList]$list) {

        # https://dev.emarsys.com/v2/email-campaign-life-cycle/send-a-test-email

        # TODO [ ] implement recipientlist, filte_id and contactlist first

        $body = @{
            subject = $subject

            # Multiple values are allowed, separated by a comma without whitespace.
            # Provide either a recipientlist, filter_id or contactlist_id. Do not combine.
            recipientlist = $list.id
            #filter_id = 0
            #contactlist_id = 0
        }
        $bodyJson = ConvertTo-Json -InputObject $body -Depth 20

        # Call emarsys
        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl)email/$( $this.id )/sendtestmail"
            method = "Post"
            body = $bodyJson
            verbose = $true
        }
        $res = Invoke-emarsys @params
        return $res

    }

    # Use this endpoint to ask for response data
    # then start polling downloadResponses within 2 minutes
    # the result is available for 2 hourse
    [int] getResponses([String]$type) {

        return $this.emarsys.getResponses($type,$this.id)

        # https://dev.emarsys.com/v2/email-campaign-life-cycle/preview-email-campaign-contents

        # TODO [ ] Put the type in an enum
        <#
        $body = @{
            "type" = $type # opened, not_opened, received, clicked, not_clicked, bounced, hard_bounced, soft_bounced, block_bounced
            #"start_date" = "YYYY-MM-DD"
            #"end_date" = "YYYY-MM-DD"
            "campaign_id" = $this.id # optional
        }
        $bodyJson = ConvertTo-Json -InputObject $body -Depth 20
 
        # Call emarsys
        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl)email/responses"
            method = "Post"
            body = $bodyJson
            verbose = $true
        }
        $res = Invoke-emarsys @params
        return $res.id
#>

    }

    [PSCustomObject] pollResponseResults([int]$queryId) {

        return $this.emarsys.pollResponseResults($queryId)
        <#
        # Response summary
        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl)email/$( $queryId )/responses"
        }
        $res = Invoke-emarsys @params
        return $res
        #>

    }

}


#-----------------------------------------------
# EXPORTS
#-----------------------------------------------

class EmarsysExport {


    #-----------------------------------------------
    # PROPERTIES (can be public by default, static or hidden)
    #-----------------------------------------------

    hidden [Emarsys]$emarsys
    [PSCustomObject]$raw        # the raw source object for this one

    [EmarsysField[]]$fields
    [EmarsysList]$list
    [String]$outputFolder

    [int]$exportId

    [String]$status
    [DateTime]$startTime
    [DateTime]$endTime
    #[int]$offset = 0
    hidden [int]$limit = 10000000 #10 #10000000 # TODO [ ] test limit
    [int]$totalSeconds = 0

    hidden [String]$filename
    hidden [String[]]$exportFiles
    hidden [Timers.Timer]$timer
    [bool] $alreadyDownloaded = $false


    #-----------------------------------------------
    # PUBLIC CONSTRUCTORS
    #-----------------------------------------------

    # empty default constructor needed to support hashtable constructor
    EmarsysExport () {
        $this.init()
    }

    #-----------------------------------------------
    # METHODS
    #-----------------------------------------------

    hidden [void] init() {
        $this.startTime = [DateTime]::Now
    }

    [String[]] getFiles() {
        return $this.exportFiles
    }

    [void] updateStatus () {

        $params = $this.emarsys.defaultParams + @{
            uri = "$( $this.emarsys.baseUrl )export/$( $this.exportId )"
        }
        $exportStatus = Invoke-emarsys @params
        #Write-Verbose ( $exportStatus | ConvertTo-Json )
        $this.status = $exportStatus.status
        $this.raw = $exportStatus

        if ( $exportStatus.status -eq "done" ) {
            $this.filename = $exportStatus.file_name
            $this.endTime =  [DateTime]::Now
            $t = New-TimeSpan -Start $this.startTime -End $this.endTime
            $this.totalSeconds = $t.TotalSeconds

        }

    }

    [void] autoUpdate() {
        $this.autoUpdate($false)
    }

    [void] autoUpdate([bool]$downloadImmediatly) {

        # Create a timer object with a specific interval and a starttime
        $this.timer = New-Object -Type Timers.Timer
        $this.timer.Interval  = 20000 # milliseconds, the interval defines how often the event gets fired
        $timerTimeout = 600 # seconds

        # Register an event for every passed interval
        Register-ObjectEvent -InputObject $this.timer  -EventName "Elapsed" -SourceIdentifier $this.exportId -MessageData @{ timeout=$timerTimeout; emarsysExport = $this ; downloadImmediatly = $downloadImmediatly } -Action {

            # Input
            $emarsysExport = $Event.MessageData.emarsysExport

            # Calculate current timespan
            $timeSpan = New-TimeSpan -Start $emarsysExport.startTime -End ( Get-Date )

            # Check current status
            $emarsysExport.updateStatus()

            If ($emarsysExport.status -eq "done" ) { # -or ( $this.raw.type -eq "responses" -and $emarsysExport.status -eq "ready")

                $Sender.stop()

                if ($Event.MessageData.downloadImmediatly) {
                    $emarsysExport.downloadResult()
                }

            }

            # Is timeout reached? Do something!
            if ( $timeSpan.TotalSeconds -gt $Event.MessageData.timeout ) {

                # Stop timer now (it is important to do this before the next processes run)
                $Sender.Stop()
                Write-Host "Done! Timer stopped because timeout reached!"

            }

        } | Out-Null

        # Start the timer
        $this.timer.Start()

    }

    [void] downloadResult() {

        # TODO [ ] unregister timer event, if it exists

        # Download file
        # TODO [ ] implement offset and limit
        # TODO [ ] export contains multiple files
        # TODO [ ] calculate time when finishing export
        if ( @("ready","done") -contains $this.status ) {

            if ($this.raw.type -eq "contactlist") {
                $listCount = $this.list.count()
                $rounds = [Math]::Ceiling($listCount/$this.limit)
            } else {
                $rounds = 1
            }

            for ( $i = 0 ; $i -lt $rounds ; $i++ ) {
                # TODO [ ] it looks like there is a bug in offset and limit, so re-visit this later
                $offset = $i * $rounds

                # Sometimes the export does not come to the status "done" so we can download it with a fictitous filename
                #if ( $this.status -eq "ready" ) {
                # $this.filename = "$( [DateTime]::Now.ToString("yyyyMMdd_HHmmss") ).csv"
                #}

                # Create the download job
                $params = $this.emarsys.defaultParams + @{
                    uri = "$( $this.emarsys.baseUrl )export/$( $this.exportId )/data" #?offset=$( $offset )&limit=$( $this.limit )"
                    outFile = "$( $this.outputFolder )\$( $this.filename )"
                }
                Invoke-emarsys @params

                # Add to the result
                $this.exportFiles += $params.OutFile
            }

            # Flag this as already downloaded
            $this.alreadyDownloaded = $true

        }

    }

}


################################################
#
# MAIN CLASS
#
################################################


class Emarsys : DCSP {

    #-----------------------------------------------
    # PROPERTIES (can be public by default, static or hidden)
    #-----------------------------------------------

    hidden [pscredential]$cred                 # holds the username and secret
    hidden [int]$waitSeconds = 10
    [String]$baseUrl = "https://api.emarsys.net/api/v2/"
    [DSCPScope[]]$supportedScopes = @(
        [DSCPScope]::Global
        #[DSCPScope]::Local
    )

    # Override inherited properties
    [String]$providerName = "emarsys"
    static [bool]$allowNewFieldCreation = $true

    [PSCustomObject]$defaultParams
    hidden [EmarsysExport[]]$exports


    #-----------------------------------------------
    # PUBLIC CONSTRUCTORS
    #-----------------------------------------------


    # empty default constructor needed to support hashtable constructor
    Emarsys () {
        $this.init()
    }

    Emarsys ( [String]$username, [String]$secret ) {
        $this.init( $username, $secret )
    }

    Emarsys ( [String]$username, [String]$secret, [String]$baseUrl ) {
        $this.init( $username, $secret, $baseUrl)
    }

    Emarsys ( [pscredential]$cred ) {
        $this.init( $cred )
    }

    Emarsys ( [pscredential]$cred, [String]$baseUrl ) {
        $this.init( $cred, $baseUrl )
    }

    #-----------------------------------------------
    # HIDDEN CONSTRUCTORS - CHAINING
    #-----------------------------------------------

    hidden [void] init () {

        $this.defaultParams = @{
            cred = $this.cred
        }

        if ( $script:settings.download.waitSecondsLoop ) {
            $this.waitSeconds = $script:settings.download.waitSecondsLoop
        }

        #$this.exports = [System.Collections.ArrayList]@()

    }

    hidden [void] init ( [String]$username, [String]$secret ) {
        $stringSecure = ConvertTo-SecureString -String ( Get-SecureToPlaintext $secret ) -AsPlainText -Force
        $this.cred = [pscredential]::new( $username, $stringSecure )
        $this.init()
    }

    hidden [void] init ( [String]$username, [String]$secret, [String]$baseUrl ) {
        $this.baseUrl = $baseUrl
        $this.init( $username, $secret )
    }

    hidden [void] init ( [pscredential]$cred ) {
        $this.cred = $cred
        $this.init()
    }

    hidden [void] init ( [pscredential]$cred, [String]$baseUrl ) {
        $this.baseUrl = $baseUrl
        $this.init( $cred )
    }



    #-----------------------------------------------
    # METHODS
    #-----------------------------------------------

    [PSCustomObject] getSettings () {

        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)settings"
        }

        $res = Invoke-emarsys @params
        return $res

    }

    [string] newField([String]$fieldname, [EmarsysFieldApplicationTypes]$dataType) {

        # TODO [] implement this one https://dev.emarsys.com/v2/fields/create-a-field

        $body = @{
            name = $fieldname
            application_type = $dataType # shorttext|longtext|largetext|date|url|numeric
            #string_id = "" # optional otherwise autogenerated
        }
        $bodyJson = ConvertTo-Json -InputObject $body -Depth 20

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)field"
            method = "Post"
            body = $bodyJson
            verbose = $true
        }
        $res = Invoke-emarsys @params

        # return the new identifier of the field
        return $res

    }

    [PSCustomObject] getFields () {
        return getFields($true)
    }



    [EmarsysField[]] getFields ([bool]$loadDetails) {

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)field"
        }
        $res = Invoke-emarsys @params


        # Transform result to objects
        $fields = [System.Collections.ArrayList]@()
        $res | ForEach {

            $f = $_

            $choice = [System.Collections.ArrayList]@()

            if ( $loadDetails ) {

                # list fields choices
                if ( $f.application_type -eq "singlechoice") {
                    $params = @{
                        cred = $this.cred
                        uri = "$( $this.baseUrl)field/$( $f.id )/choice"
                    }
                    $choices = Invoke-emarsys @params
                    $choices | ForEach {
                        $c = $_
                        [void]$choice.Add([DCSPFieldChoice]@{
                            "id" = $c.id
                            "label" = $c.choice
                        })
                    }
                    # TODO [ ] check bit_position in return data
                }

                # TODO [ ] check multiple choice which is called with /choices

            }

            $fields.Add([EmarsysField]@{
                "emarsys" = $this
                "id" = $f.id
                "name" = $f.string_id
                "label" = $f.name
                "dataType" = $f.application_type
                #"scope" = [DSCPScope]::Global
                "choices" = $choice
            })

            $choice.Clear()

        }



        # Return the results
        return $fields

    }

    [EmarsysList[]] getLists () {

        # https://dev.emarsys.com/v2/contact-lists/count-contacts-in-a-contact-list
        # TODO [ ] implement as classes with create, rename, delete, count, list contacts, list contacts data, add contacts, lookup(?)

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)contactlist"
        }
        $res = Invoke-emarsys @params


        # Transform result to objects
        $lists = [System.Collections.ArrayList]@()
        $res | ForEach {

            $l = $_

            [void]$lists.Add([EmarsysList]@{
                "emarsys" = $this
                "id" = $l.id
                "name" = $l.name
                "created" = $l.created
                "type" = $l.type
                "raw" = $l
            })

        }

        return $lists

    }


    [PSCustomObject] getSegments () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)filter"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getSources () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)source"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] createContact ([String]$keyId, [String]$contactListId, [System.Collections.ArrayList]$arr) {

        # TODO [ ] currently only for test/dev

        $body = [PSCustomObject]@{
            "key_id" = $keyId
            "contacts" = $arr
            "contact_list_id" = $contactListId
        }

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )contact/?create_if_not_exists=1"
            method = "Put"
            body = ConvertTo-Json -InputObject $body -Depth 20
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] deleteContact ([String]$keyId, [System.Collections.ArrayList]$arr) {

        # TODO [ ] currently only for test/dev

        $body = [PSCustomObject]@{
            "key_id" = $keyId
            "$( $keyId )" = $arr
            #"contact_list_id" = $contactListId
        }

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )contact/delete"
            method = "Post"
            body = ConvertTo-Json -InputObject $body -Depth 20
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] deleteContactFromList ([String]$keyId, [Int]$contactListId, [System.Collections.ArrayList]$arr) {

        # TODO [ ] currently only for test/dev

        $body = [PSCustomObject]@{
            "key_id" = $keyId
            "$( $keyId )" = $arr
            "contact_list_id" = $contactListId
        }

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )contact/delete"
            method = "Post"
            body = ConvertTo-Json -InputObject $body -Depth 20
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] createList ([String]$keyId, [String]$name, [String]$description) {

        # TODO [ ] currently only for test/dev

        $body = [PSCustomObject]@{
            "key_id" = $keyId
            "name" = $name
            "description" = $description
            "external_ids" = [Array]@()
        }

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )contactlist"
            method = "Post"
            body = ConvertTo-Json -InputObject $body -Depth 20
        }
        $res = Invoke-emarsys @params
        return $res

    }


    [PSCustomObject] fetchListContacts ([String]$listId) {

        # TODO [ ] currently only for test/dev
        # TODO [ ] will be deprecated End of 2024

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )contactlist/$( $listId )/contacts"
            method = "Get"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getContactData ([String]$keyId, [System.Collections.ArrayList]$fields, [System.Collections.ArrayList]$keyValues) {

        # TODO [ ] currently only for test/dev

        $body = [PSCustomObject]@{
            "keyId" = $keyId
            "fields" = $fields
            "keyValues" = $keyValues
        }

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )contact/getdata"
            method = "Post"
            body = ConvertTo-Json -InputObject $body -Depth 20

        }
        $res = Invoke-emarsys @params
        return $res

    }


    [PSCustomObject] countList ([String]$listId) {

        # TODO [ ] currently only for test/dev

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )contactlist/$( $listId )/count" #https://api.emarsys.net/api/v2/contactlist/{listId}/count
            method = "Get"
        }
        $res = Invoke-emarsys @params
        return $res

    }



    [EmarsysMailing[]] getEmailCampaigns () {

        # https://dev.emarsys.com/v2/contact-lists/count-contacts-in-a-contact-list
        # TODO [ ] implement as classes with the toString-function, list tracked links, list sections, preview

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)email" # ?status={{status}}&launched={{launched}}&contactlist={{contactlist}}&showdeleted={{showdeleted}}&fromdate={{fromdate}}&todate={{todate}}&root_campaign_id={{root_campaign_id}}&template={{template}}&content_type={{content_type}}&campaign_type={{campaign_type}}&parent_campaign_id={{parent_campaign_id}}&behavior_channel={{behavior_channel}}&email_category={{email_category}}
        }
        $res = Invoke-emarsys @params

        # Transform result to objects
        $campaigns = [System.Collections.ArrayList]@()
        $res | ForEach {

            $c = $_

            [void]$campaigns.Add([EmarsysMailing]@{

                "id" = $c.id
                "name" = $c.name
                "created" = $c.created

                "subject" = $c.subject
                "fromEmail" = $c.fromemail
                "fromName" = $c.fromname
                "contentType" = [DCSPMailingsEmailContentTypes]::($c."content_type")

                "language" = $c.language
                "emarsys" = $this
                "raw" = $c

            })

        }

        return $campaigns

    }

    # TODO [ ] implement media database if needed

    [PSCustomObject] getConditionalTextRules () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)condition"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getEmailTemplates () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)email/templates"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getLinkCategories () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)settings/linkcategories"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getExternalEvents () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)event"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getAutomationCenterPrograms () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)ac/programs"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getAutoImportProfiles () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)settings/autoimports"
        }
        $res = Invoke-emarsys @params
        return $res

    }

    [PSCustomObject] getEmailCategories () {

        # TODO [ ] implement as classes

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)emailcategory"
        }
        $res = Invoke-emarsys @params
        return $res

    }



    [EmarsysExport[]] downloadContactList ( [EmarsysList]$list, [String]$outputFolder ) {

        $exportJobs = [System.Collections.ArrayList]@()

        # split the fields automatically
        # TODO [ ] find out if the primary key is always included

        $fields = $this.getFields($false) | where { $_.excludeForExport -eq $false }

        # paging through fields and create exports
        $count = $fields.count
        $maxFields = 20 # max from emarsys
        $rounds = [System.Math]::Ceiling($count/$maxFields)
        for ( $i = 0 ; $i -lt $rounds ; $i++ ) {
            $start = $i * $maxFields
            $end = ( ( $i + 1 ) * $maxFields ) -1
            $exportFields = $fields[$start..$end]
            $emarsysExport = $this.downloadContactList($list,$exportFields,$outputFolder)
            $exportJobs.Add( $emarsysExport )
            #$this.exports += $emarsysExport
        }

        #$this.exports.AddRange($exportJobs)

        return $exportJobs

    }

    [EmarsysExport[]] getExports() {
        return $this.exports
    }

    # Download the contacts synchronously
    [EmarsysExport] downloadContactList ( [EmarsysList]$list, [EmarsysField[]]$fields, [String]$outputFolder ) {

        # TODO [ ] implement as classes
        # TODO [ ] make delimiter available as enum
        # TODO [ ] implement language

        $exportFields = $fields | where { $_.excludeForExport -eq $false }

        # Create export
        $body = @{
            distribution_method = "local"
            contactlist = $list.id
            contact_fields = $exportFields.id # # field ids -> max 20 columns, exclude of 27, 28, 29, 32 and 33
            delimiter = ";" # ,|;
            add_field_names_header = 1
            #language = "de"
        }

        # Call emarsys to create export job
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )email/getcontacts"
            method = "Post"
            body = ConvertTo-Json -InputObject $body -Depth 20
        }
        $exportId = Invoke-emarsys @params

        # Create the export object now
        $export = ( [EmarsysExport]@{

            "emarsys" = $this
            "raw" = $exportId

            "outputFolder" = $outputFolder
            "fields" = $fields
            "list" = $list

            "exportId" = $exportId.id

        })

        $this.exports += $export

        return $export
    }

    [EmarsysExport] downloadSegment ([String]$outputFolder) {
        # https://dev.emarsys.com/v2/contact-and-email-data/export-a-segment
        # TODO [ ] implement this
        return [EmarsysExport]@{}
    }

    [EmarsysExport] downloadRegistrations ([String]$outputFolder) {
        # https://dev.emarsys.com/v2/contact-and-email-data/export-contact-registrations
        # TODO [ ] implement this
        return [EmarsysExport]@{}
    }


    [EmarsysExport] downloadResponses ([String]$outputFolder) {

        # TODO [ ] implement the response download

        # https://dev.emarsys.com/v2/contact-and-email-data/export-responses
        $body = @{
            distribution_method = "local"
            time_range = @(
                ( Get-Date -Year 2022 -Month 2 -Day 15 -Hour 0 -Minute 0 -Second 0 ).ToString("yyyy-MM-dd HH:mm:ss")
                ( Get-Date -Year 2022 -Month 2 -Day 15 -Hour 23 -Minute 59 -Second 59 ).ToString("yyyy-MM-dd HH:mm:ss")
                #[DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss")
            ) #YYYY-MM-DD HH-SS
            contact_fields = @(
                1
                3 # 3 is email
            ) # the field identifiers to include in the export.
            sources = @(
                #"trackable_links"
                #"registration_forms"
                #"tell_a_friend"
                #"contact_us"
                #"change_profile"
                "unsubscribe"
                "mail_open"
                #"complaint"
            )
            analysis_fields = @(
                #1 # Campaign title
                #2 # Section header
                #3 # Section group
                #4 # Link title
                5   # URL
                8   # Time
                #12 # Campaign identifier
                #13 # Version name
                #14 # Campaign category
                15  # Link category
            )

            # optional
            #email_id = 100146526 # The identifier of the email campaign. Returns the contact's responses to the email.
            #contactlist = 786367148 #$list.id # The identifier of the contact list to filter the results.
            delimiter = ";" # ,|;
            add_field_names_header = 1 # Determines whether to insert a header row into the CSV file.
            language = "en"
        }

        # Call emarsys to create export job
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl )email/getresponses"
            method = "Post"
            body = ConvertTo-Json -InputObject $body -Depth 20
        }
        $exportId = Invoke-emarsys @params

        # TODO [ ] load the body / parameters into the Export object, too?

        # Create the export object now
        $export = ( [EmarsysExport]@{

            "emarsys" = $this
            "raw" = $exportId
            "outputFolder" = "."

            #"fields" = $fields
            #"list" = $list

            "exportId" = $exportId.id

        })

        $this.exports += $export

        return $export

    }

    [int] getResponses([String]$type) {
        return $this.getResponses($type,0)
    }

    # Use this endpoint to ask for response data
    # then start polling downloadResponses within 2 minutes
    # the result is available for 2 hourse
    [int] getResponses([String]$type, [int]$campaignId) {

        # https://dev.emarsys.com/v2/email-campaign-life-cycle/preview-email-campaign-contents

        # TODO [ ] Put the type in an enum

        $body = @{
            "type" = $type # opened, not_opened, received, clicked, not_clicked, bounced, hard_bounced, soft_bounced, block_bounced
            #"start_date" = "YYYY-MM-DD"
            #"end_date" = "YYYY-MM-DD"
            #"campaign_id" = $this.id # optional
        }
        if ( $campaignId -gt 0 ) {
            $body | Add-Member -MemberType NoteProperty -Name "campaign_id" -Value $campaignId
        }
        $bodyJson = ConvertTo-Json -InputObject $body -Depth 20

        # Call emarsys
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)email/responses"
            method = "Post"
            body = $bodyJson
            verbose = $true
        }
        $res = Invoke-emarsys @params
        return $res.id

    }

    [PSCustomObject] pollResponseResults([int]$queryId) {

        # Response summary
        $params = $this.defaultParams + @{
            uri = "$( $this.baseUrl)email/$( $queryId )/responses"
        }
        $res = Invoke-emarsys @params
        return $res

    }

}


################################################
#
# OTHER FUNCTIONS
#
################################################

function Invoke-emarsys {

    [CmdletBinding()]
    param (
         [Parameter(Mandatory=$false)][pscredential]$cred                                   # securestring containing username as user and secret as password
        ,[Parameter(Mandatory=$false)][System.Uri]$uri = "https://api.emarsys.net/api/v2/"  # default url to use
        ,[Parameter(Mandatory=$false)][String]$method = "Get"
        ,[Parameter(Mandatory=$false)][String]$outFile = ""
        ,[Parameter(Mandatory=$false)][System.Object]$body = $null
    )

    begin {


        #-----------------------------------------------
        # AUTH
        #-----------------------------------------------

        <#
 
        example for header
 
        X-WSSE: UsernameToken
        Username="customer001",
        PasswordDigest="ZmI2ZmQ0MDIxYmNwQjcxNDkxY2RjNDNiMWExNjFkZA==",
        Nonce="d36e316282959a9ed4c72351497a717f",
        Created="2014-03-20T12:51:45Z"
 
        source: https://dev.emarsys.com/v2/before-you-start/authentication
        api endpoints: https://trunk-int.s.emarsys.com/api-demo/#tab-customer
 
        other urls
        https://dev.emarsys.com/v2/emarsys-developer-hub/what-is-the-emarsys-api
        #>


        # Extract credentials
        $secret = $cred.GetNetworkCredential().Password
        $username = $cred.UserName

        # Create nonce
        $randomStringAsHex = Get-RandomString -length 16 | Format-Hex
        $nonce = Get-StringfromByte -byteArray $randomStringAsHex.Bytes

        # Format date
        $date = [datetime]::UtcNow.ToString("o")

        # Create password digest
        $stringToSign = $nonce + $date + $secret
        $sha1 = Get-StringHash -inputString $stringToSign -hashName "SHA1"
        $passwordDigest = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($sha1))

        # Combine Escher XWSSE header
        $xwsseArr = [System.Collections.ArrayList]@()
        [void]$xwsseArr.Add("UsernameToken Username=""$( $username )""")
        [void]$xwsseArr.Add("PasswordDigest=""$( $passwordDigest )""")
        [void]$xwsseArr.Add("Nonce=""$( $nonce )""")
        [void]$xwsseArr.Add("Created=""$( $date )""")

        # Setup content type
        $contentType = "application/json;charset=utf-8"
        #$xwsseArr.Add("Content-type=""$( $contentType )""") # take this out possibly

        # Join Escher XWSSE together
        $xwsse = $xwsseArr -join ", "
        #$xwsse


        #-----------------------------------------------
        # HEADER
        #-----------------------------------------------

        $header = @{
            "X-WSSE"=$xwsse
            "X-Requested-With"=    "XMLHttpRequest"
        }

    }

    process {


        $params = @{
            "Uri" = $uri
            "Method" = $method
            "Headers" = $header
            "ContentType" = $contentType
            "Verbose" = $true
        }

        if ( $body -ne $null ) {
            $params += @{
                "Body" = $body
            }
        }

        if ( $outFile -ne "" ) {
            $params += @{
                "OutFile" = $outFile
            }
        }

        $result = Invoke-RestMethod @params #-UseBasicParsing

    }

    end {

        if ( $outFile -ne "" ) {

            $outFile

        } else {

            if ( $result.replyCode -eq 0 <# -and $result.replyText -eq "OK" #> ) {

                $result.data

            } else {
                # Errors see here: https://dev.emarsys.com/v2/response-codes/http-400-errors
                Write-Log -message "Got back $( $result.replyText ) from call to url $( $uri ), throwing exception"
                throw [System.IO.InvalidDataException]

            }

        }

    }

}