plugins/CleverReach/Public/response/get-responsebyday.ps1
<# Use this like ```PowerShell $startDate = [DateTime]::ParseExact("2025-06-15","yyyy-MM-dd",$null) $endDate = [DateTime]::ParseExact("2025-06-20","yyyy-MM-dd",$null) #[DateTime]::Today.AddDays(-1) $days = 0 Do { $today = $StartDate.AddDays($days) $today.toString("yyyy-MM-dd") Get-ResponseByDay -MessageStartDate $today.AddDays(-30) -MessageEndDate $today.AddDays(1).AddSeconds(-1) -ResponseStartDate $today -ResponseEndDate $today.AddDays(1).AddSeconds(-1) $days += 1 } Until ( $today -eq $endDate ) ``` #> function Get-ResponseByDay { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [datetime]$MessageStartDate ,[Parameter(Mandatory=$true)] [datetime]$MessageEndDate #[Parameter(Mandatory=$false)][Hashtable] $InputHashtable ,[Parameter(Mandatory=$true)] [datetime]$ResponseStartDate ,[Parameter(Mandatory=$true)] [datetime]$ResponseEndDate #= [DateTime]::Today.AddDays(-1).ToString("yyyy-MM-dd") ) begin { #----------------------------------------------- # START TIMER #----------------------------------------------- $processStart = [datetime]::now #$inserts = 0 #----------------------------------------------- # LOG #----------------------------------------------- # Override logfile, if this is set to true If ( $Script:settings.responses.useSeparateLogfile -eq $true ) { Set-Logfile -Path $Script:settings.responses.logfile } $moduleName = "GETRESPONSE" # Start the log Write-Log -message $Script:logDivider Write-Log -message $moduleName -Severity INFO # Log the params, if existing Write-Log -message "INPUT:" if ( $InputHashtable ) { $InputHashtable.Keys | ForEach-Object { $param = $_ Write-Log -message " $( $param ) = '$( $InputHashtable[$param] )'" -writeToHostToo $false } } #----------------------------------------------- # DEBUG MODE #----------------------------------------------- Write-Log "Debug Mode: $( $Script:debugMode )" #----------------------------------------------- # TIMESTAMPS FOR LOADING MAILING REPORTS AND RESPONSES #----------------------------------------------- # The end of request is now! $endTimestamp = Get-Unixtime # Load the response timestamp if available $responseStartTimestamp = Get-Unixtime -timestamp $ResponseStartDate #Get-Unixtime -timestamp ([DateTime]::Now.AddDays( $Script:settings.responses.responsePeriod *-1 )) Write-Log "Using this timestamp for responses start: $( $responseStartTimestamp )" $responseEndTimestamp = Get-Unixtime -timestamp $ResponseEndDate #Get-Unixtime -timestamp ([DateTime]::Now.AddDays( $Script:settings.responses.responsePeriod *-1 )) Write-Log "Using this timestamp for responses end: $( $responseEndTimestamp )" # Settings for messages $messageStartTimestamp = Get-Unixtime -timestamp $MessageStartDate Write-Log "Using this timestamp for messages start: $( $messageStartTimestamp )" # 01. Juli $messageEndTimestamp = Get-Unixtime -timestamp $MessageEndDate Write-Log "Using this timestamp for messages end: $( $messageEndTimestamp )" # 01. Juli #Write-Log "Using this timestamp for end: $( $endTimestamp )" #----------------------------------------------- # DETAILS DEFINITION #----------------------------------------------- $cleverreachDetailsBinaryValues = [Hashtable]@{ events = 1 orders = 2 tags = 4 } #----------------------------------------------- # CHECK CLEVERREACH CONNECTION #----------------------------------------------- try { Test-CleverReachConnection } catch { #$msg = "Failed to connect to CleverReach, unauthorized or token is expired" #Write-Log -Message $msg -Severity ERROR Write-Log -Message $_.Exception -Severity ERROR throw [System.IO.InvalidDataException] $msg exit 0 } #Write-Log -Message "Debug Mode: $( $Script:debugMode )" } process { try { #----------------------------------------------- # WORKOUT THE NEEDED DETAIL BINARY VALUE FOR RECEIVERS #----------------------------------------------- $cleverReachDetailsBinary = 0 $cleverreachDetailsBinaryValues.Keys | ForEach-Object { if ( $Script:settings.loadDetails.($_) -eq $true ) { $cleverReachDetailsBinary += $cleverreachDetailsBinaryValues[$_] } } #----------------------------------------------- # DOWNLOAD GLOBAL ATTRIBUTES #----------------------------------------------- $globalUrnAttribute = $false $globalAttributes = @( (Invoke-CR -Object "attributes" -Method "GET" ) ) $urnField = @( $globalAttributes | Where-Object { $_.name -eq $Script:settings.responses.urnFieldName } ) If ( $urnField.count -eq 0 ) { Write-Log -message "Looks like the urn is not on global level. It tries to load it from local attributes" } elseif ( $urnField.count -eq 1 ) { $globalUrnAttribute = $true Write-Log -message "Looks like the urn is on global level named $( $urnField.description )" } else { Write-Log -message "There are multiple urnfields on global level!? Please check" -severity WARNING } #----------------------------------------------- # DOWNLOAD REPORTS #----------------------------------------------- Write-Log -message "Downloading reports" $reportsQuery = [PSCustomObject]@{ start = $messageStartTimestamp end = $messageEndTimestamp } $reports = @( Invoke-CR -Object "reports" -Query $reportsQuery -Method "GET" -Paging -Pagesize 80 ) Write-Log -message "Found $( $reports.Count ) reports" #for the last $( $Script:settings.responses.messagePeriod ) days" ################################################ # # DOWNLOAD ALL REPORTS RECEIVERS # ################################################ <# IMPORTANT HINT The events attached to a receiver are only the last 250 entries... this is the reason why we need for every state, every mailing and every link #> $responseTypes = [Hashtable]@{ sent = $Script:settings.responses.loadSent opened = $Script:settings.responses.loadOpens clicked = $Script:settings.responses.loadClicks notopened = $false notclicked = $false bounced = $Script:settings.responses.loadBounces unsubscribed = $Script:settings.responses.loadUnsubscribes } # Going through all response types like opens and clicks $responseCounts = [PSCustomObject]@{} $allLinks = [System.Collections.ArrayList]@() $responseTypes.Keys | ForEach-Object { # response type like sent, opened, ... $responseType = $_ # check if this response type should be downloaded if ( $responseTypes[$responseType] -eq $true ) { # create an array to put the results in $responses = [System.Collections.ArrayList]@() # go through every mailing of last n days and check for responses $reports | ForEach-Object { $reportId = $_.id Write-Log -message "Downloading report id '$( $reportId )' and response type '$( $responseType )'" # load links for this report that are clicked more than 0 and add them to the all links collection $iLink = 0 if ( $responseType -eq "clicked" ) { #https://rest.cleverreach.com/v3/reports.json/8148376/stats/links $links = @( (Invoke-CR -Object "reports" -Path "/$( $reportId )/stats/links" | Where-Object { $_.links.total_clicks -gt 0 }).links ) #$links = @( Invoke-CR -Object "mailings" -Path "/$( $reportId )/links" -Method "GET" -Verbose ) #Invoke-RestMethod -Method Get -Uri "$( $settings.base)mailings.json/$( $reportId )/links" -Headers $header $allLinks.AddRange($links) } Do { # Add some parameters for loading details $query = [PSCustomObject]@{ "detail" = $cleverReachDetailsBinary "from" = $responseStartTimestamp "to" = $responseEndTimestamp } # Ask for a specific link id if ( $responseType -eq "clicked" ) { $linkId = $links[$iLink].id #$attachLink = "&linkid=$( $linkId )" $query | Add-Member -MemberType NoteProperty -Name "linkid" -Value $linkId $iLink += 1 Write-Log -message " Downloading link $( $linkId )" } $result = @( Invoke-CR -Object "reports" -Path "/$( $reportId )/receivers/$( $responseType )" -Query $query -Method "GET" -Paging ) #$script:pluginDebug = $result If ( $result.count -gt 0 ) { $responses.AddRange(@( $result | Select-Object @{name="state";expression={ $responseType }},@{name="report";expression={ $reportId }},@{name="linkid";expression={ $linkId }}, * )) #$allResponses.AddRange(@( $result | Select @{name="state";expression={ $responseType }},@{name="report";expression={ $reportId }},@{name="linkid";expression={ $linkId }}, * )) } Write-Log -message " Got $( $result.count ) results" # Do another round if there are links left because we need to ask for every link } while ( $iLink -lt ( $links.count ) -and $responseType -eq "clicked" ) } Write-Log -message "Now going trough $( $responses.count ) responses of type '$( $responseType )'" # Now save the reactions directly into a file $responsesResolved = [System.Collections.ArrayList]@() $responses | foreach-object { $r = $_ # Work out the urn: global -> local -> crID $urn = "" If ( $globalUrnAttribute -eq $true ) { $urn = $r.global_attributes.( $Script:settings.responses.urnFieldName ) } else { $urn = $r.attributes.( $Script:settings.responses.urnFieldName ) } # Fallback for id If ($null -eq $urn -or $urn.length -eq 0) { $urn = $r.id } # The minimum of the object to report $responseObj = [Ordered]@{ #"MessageType" = $responseType "urn" = $urn # TODO think about urn column in $Script:settings.responses.urnFieldName "email" = $r.email "mailingId" = $r.report "mailingName" = ($reports | Where-Object { $_.id -eq $r.report }).name "timestamp" = 0 # default value that can be overriden #"communicationkey" = $r.attributes."$( $Script:setttings.responses.communicationKeyAttributeName )" # not used now as the matching should be done through email address and broadcast id } # work out the filter for the events of this receiver Switch ( $responseType ) { "sent" { <# { "stamp": 1688027454, "type": "mail_send", "type_id": "8157881", "value": "", "mailing_id": "8157881", "groups_id": "0" } #> $type = "Send" #Open, Click, Bounce, Unsubscription, Send $events = @( $r.events | Where-Object { $_.type -eq "mail_send" -and $_.mailing_id -eq $r.report } ) $responseObj.timestamp = ( $reports | Where-Object { $_.id -eq $r.report } ).finished # TODO maybe convert into other datetime format } "opened" { <# { "stamp": 1674205127, "type": "mail_open", "type_id": "8071313", "value": "95.223.77.119", "mailing_id": "8071313", "groups_id": "0" } #> $type = "Open" #Open, Click, Bounce, Unsubscription, Send $events = @( $r.events | Where-Object { $_.type -eq "mail_open" -and $_.mailing_id -eq $r.report } ) } "clicked" { <# { "stamp": 1674204151, "type": "mail_click", "type_id": "41412194", "value": "40.94.94.6", "mailing_id": "8071313", "groups_id": "0" } #> $type = "Click" #Open, Click, Bounce, Unsubscription, Send $events = @( $r.events | Where-Object { $_.type -eq "mail_click" -and $_.mailing_id -eq $r.report -and $_.type_id -eq $r.linkid } ) # possibly filter on stamp -gt $responsestartdate #$responseObj | Add-Member -MemberType NoteProperty -Name "link" -Value ( $allLinks | Where-Object { $_.id -eq $r.linkid } ).link $responseObj.Add("link", ( $allLinks | Where-Object { $_.id -eq $r.linkid } ).link ) } "bounced" { <# { "stamp": 1688027464, "type": "mail_bounce", "type_id": "hardbounce", "value": "smtp; 550 5.4.1 Recipient address rejected: Access denied. AS(201806281) [HE1EUR01FT103.eop-EUR01.prod.protection.outlook.com 2023-06-29T08:30:52.649Z 08DB7741F2946D97]", "mailing_id": "8157881", "groups_id": "0" } #> $type = "Bounce" #Open, Click, Bounce, Unsubscription, Send $events = @( $r.events | Where-Object { $_.type -eq "mail_bounce" -and $_.mailing_id -eq $r.report } ) # possibly filter on stamp -gt $responsestartdate } "unsubscribed" { <# { "stamp": 1688028443, "type": "user_unsubscribe", "type_id": "228563", "value": "34.219.234.42", "mailing_id": "8157881", "groups_id": "1146810" } #> $type = "Unsubscription" #Open, Click, Bounce, Unsubscription, Send $events = @( $r.events | Where-Object { $_.type -eq "user_unsubscribe" -and $_.mailing_id -eq $r.report } ) # possibly filter on stamp -gt $responsestartdate } } # Now add the type, readable for FERGE #$responseObj | Add-Member -MemberType NoteProperty -Name "MessageType" -Value $type $responseObj.Add("MessageType", $type) # add at minimum one entry to the collection If ( $events.count -ge 1 ) { # There is 1 or multiple events available, so copy the current receivers object and add all of the events like multiple clicks $events | ForEach-Object { $e = $_ $eventObj = [Ordered]@{} #$responseObj.psobject.copy() $responseObj.GetEnumerator() | ForEach-Object { $eventObj.Add($_.Name, $_.Value) } $eventObj.timestamp = $e.stamp # TODO convert into other datetime format ? If ($responseType -eq "bounced") { $eventObj.Add("bouncetype", $e.type_id) $eventObj.Add("bouncereason", $e.value) #$eventObj | Add-Member -MemberType NoteProperty -Name "bouncetype" -Value $e.type_id #$eventObj | Add-Member -MemberType NoteProperty -Name "bouncereason" -Value $e.value } If ($responseType -eq "unsubscribed") { $eventObj.Add("groupid", $e.groups_id) #$eventObj | Add-Member -MemberType NoteProperty -Name "groupid" -Value $e.groups_id } [void]$responsesResolved.add([PSCustomObject]$eventObj) } } else { # No event available, just add it with timestamp of mailing [void]$responsesResolved.add([PSCustomObject]$responseObj) } } # Export as a file per response type If ( $responsesResolved.Count -gt 0 ) { $responsesResolved | Export-Csv -path ".\$( $Script:settings.responses.filePrefix )$( $responseType ).csv" -Delimiter "`t" -Encoding UTF8 -NoTypeInformation -Append } # Add results counts to a variable $responseCounts | Add-Member -MemberType NoteProperty -Name $responseType -Value $responsesResolved.count } } Write-Log -message "Done with downloading responses" $totalResponses = 0 $responseCounts.psobject.properties | ForEach-Object { Write-Log -message " $( $_.Name ): $( $_.Value )" $totalResponses += $_.Value } #$script:pluginDebug = $allResponses ################################################ # # WRAP UP # ################################################ Write-Log -message "Exporting the data into CSV and creating a folder with the id $( $processId )" # Trigger FERGE if there are responses If ( $Script:settings.responses.triggerFerge -eq $true -and $totalResponses -gt 0 ) { Write-Log "Triggering FERGE to bring responses into the database" Start-Process $Script:settings.responses.fergePath -WorkingDirectory "." Start-Process -FilePath $Script:settings.responses.fergePath -ArgumentList $Script:settings.responses.fergeConfigurationXml } } catch { $msg = "Error during uploading data. Abort!" Write-Log -Message $msg -Severity ERROR -WriteToHostToo $false Write-Log -Message $_.Exception -Severity ERROR throw $_ } finally { # Close the file reader, if open # If the variable is not already declared, that shouldn't be a problem try { $reader.Close() } catch { } #----------------------------------------------- # STOP TIMER #----------------------------------------------- $processEnd = [datetime]::now $processDuration = New-TimeSpan -Start $processStart -End $processEnd Write-Log -Message "Needed $( [int]$processDuration.TotalSeconds ) seconds in total" #Write-Host "Uploaded $( $j ) record. Confirmed $( $tagcount ) receivers with tag '$( $tags )'" } } end { } } |