Created
January 8, 2016 21:00
-
-
Save jgregmac/cbeb224ef90f78dff5f3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #===================================================================================================== | |
| # AUTHOR: J. Greg Mackinnon, Adapted from 1.1 release by Tao Yang | |
| # DATE: 2013-05-21 | |
| # Name: SCOMEnhancedEmailNotification.PS1 | |
| # Version: 3.0 | |
| # COMMENT: SCOM Enhanced Email notification which includes detailed alert information | |
| # Update: 2.0 - 2012-06-30 - Major revision for compatibility with SCOM 2012 | |
| # - Cmdlets updated to use 2012 names | |
| # - "Notified" Resolution Status logic removed | |
| # - Snapin Loading and PSDrive Mappings removed (replaced with Module load) | |
| # - HTML Email reformatted for readability | |
| # - Added '-format' parameter to allow for alphanumeric pager support | |
| # - Added '-diag' boolean parameter to create options AlertID-based diagnostic logs | |
| # Update: 2.2 - 2013-05-16 - Added logic to update "CustomField1" alert data to reflect that notification has been sent for new alerts. | |
| # - Added logic to update "CustomField2" alert data to reflect the repeat count for new alert notification sends. | |
| # - Added support for specifying alerts with resolution state "acknowledged" | |
| # - Did some minor adjustments to improve execution time and reduce memory overhead. | |
| # Update: 3.0 - 2013-05-20 - Updated to reduce volume of PowerShell instance spawned by SCOM. Added "mailTo" and "pageTo" paramerters to allow sending of both short and long messages from a single script instance. | |
| # - Converted portions of script to subroutine-like functions to allow repetition (buildHeaders, buildPage, buildMail) | |
| # - Restored "Notified" resolution state logic. | |
| # - Renamed several variables for my own sanity. | |
| # - Added article lookup updates from Tao Yang 2.0 script. | |
| # Usage: .\SCOMEnhancedEmailNotification.ps1 -alertID xxxxx -mailTo @('John Doe;[email protected]','Richard Roe;[email protected]') -pageTo @('Team Pager;[email protected]') | |
| #===================================================================================================== | |
| #In OpsMgr 2012, the AlertID parameter passed in is '$Data/Context/DataItem/AlertId$' (single quote) | |
| #Quotation marks are required otherwise the AlertID parameter will not be treated as a string. | |
| param( | |
| [string]$alertID = $(throw 'A valid, quote-delimited, SCOM AlertID must be provided for -AlertID.'), | |
| [string[]]$mailto, | |
| [string[]]$pageto, | |
| [switch]$diag | |
| ) | |
| Set-PSDebug -Strict | |
| #### Setup Error Handling: #### | |
| $error.clear() | |
| $erroractionpreference = "SilentlyContinue" | |
| #$erroractionpreference = "Inquire" | |
| #### Setup local option variables: #### | |
| ## Logging: | |
| #Remove '$alertID' from the following two log file names to prevent the drive from filling up with diag logs: | |
| $errorLogFile = 'C:\local\logs\SCOMNotifyErr-' + $alertID + '.log' | |
| $diagLogFile = 'C:\local\logs\SCOMNotifyDiag-' + $alertID + '.log' | |
| #$errorLogFile = 'C:\local\logs\SCOMNotifyErr.log' | |
| #$diagLogFile = 'C:\local\logs\SCOMNotifyDiag.log' | |
| ## Mail: | |
| $SMTPHost = "smtp.uvm.edu" | |
| $SMTPPort = 25 | |
| $Sender = New-Object System.Net.Mail.MailAddress("[email protected]", "Lifeboat OpsMgr Notification") | |
| #If error occured while excuting the script, the recipient for error notification email. | |
| $ErrRecipient = New-Object System.Net.Mail.MailAddress("[email protected]", "SAA Windows Administration Team") | |
| ##Set Culture Info (for knowledgebase article language selection): | |
| $cultureInfo = [System.Globalization.CultureInfo]'en-US' | |
| ##Get the FQDN of the local computer (where the script is run)... | |
| $RMS = $env:computername | |
| #### Initialize Global Variables and Objects: #### | |
| ## Mail Message Object: | |
| [string] $threadID = '<' + $alertID + '@uvm.edu>' | |
| $SMTPClient = New-Object System.Net.Mail.smtpClient | |
| $SMTPClient.host = $SMTPHost | |
| $SMTPClient.port = $SMTPPort | |
| ##Load SCOM PS Module | |
| if ((get-module | ? {$_.name -eq 'OperationsManager'}) -eq $null) { | |
| #Try using the full path to the OpsMgr module to avoid module loading errors: | |
| [string] $OMModPath = "D:\Program Files\Microsoft System Center 2012 R2\Operations Manager\Powershell\OperationsManager\OperationsManager.psm1" | |
| Import-Module $OMModPath -ErrorAction SilentlyContinue -ErrorVariable Err | Out-Null | |
| } | |
| ## Management Group Object: | |
| $mg = get-SCOMManagementGroup | |
| ##Get Web Console URL | |
| $WebConsoleBaseURL = (get-scomwebaddresssetting | Select-Object -Property WebConsoleUrl).webconsoleurl | |
| #### End Initialize #### | |
| #### Begin Parse Input Parameters: #### | |
| ##Get recipients names and email addresses from "-to" array parameter: ## | |
| if ((!$mailTo) -and (!$pageTo)) { | |
| write-host "An array of name/email address pairs must be provided in either the -mailTo or -pageTo parameter, in the format `@(`'me;[email protected]`',`'you;[email protected]`')" | |
| exit | |
| } | |
| $mailRecips = @() | |
| Foreach ($item in $mailTo) { | |
| $to = New-Object psobject | |
| $name = ($item.split(";"))[0] | |
| $email = ($item.split(";"))[1] | |
| Add-Member -InputObject $to -MemberType NoteProperty -Name Name -Value $name | |
| Add-Member -InputObject $to -MemberType NoteProperty -Name Email -Value $email | |
| $mailRecips += $to | |
| Remove-Variable to | |
| Remove-Variable name | |
| Remove-Variable email | |
| } | |
| $pageRecips = @() | |
| Foreach ($item in $pageTo) { | |
| $to = New-Object psobject | |
| $name = ($item.split(";"))[0] | |
| $email = ($item.split(";"))[1] | |
| Add-Member -InputObject $to -MemberType NoteProperty -Name Name -Value $name | |
| Add-Member -InputObject $to -MemberType NoteProperty -Name Email -Value $email | |
| $pageRecips += $to | |
| Remove-Variable to | |
| Remove-Variable name | |
| Remove-Variable email | |
| } | |
| if ($diag -eq $true) { | |
| [string] $("mailRecipients:") | Out-File $diagLogFile -Append | |
| $mailRecips | Out-File $diagLogFile -Append | |
| [string] $("pageRecipients:") | Out-File $diagLogFile -Append | |
| $pageRecips | Out-File $diagLogFile -Append | |
| } | |
| ## Parse "-AlertID" input parameter: ## | |
| $alertID = $alertID.toString() | |
| #remove "{" and "}" around the $alertID if exist | |
| if ($alertID.substring(0,1) -match "{") { | |
| $alertID = $alertID.substring(1, ( $alertID.length -1 )) | |
| } | |
| if ($alertID.substring(($alertID.length -1), 1) -match "}") { | |
| $alertID = $alertID.substring(0, ( $alertID.length -1 )) | |
| } | |
| #### End Parse input parameters #### | |
| #### Function Library: #### | |
| function getResStateName($resStateNumber){ | |
| [string] $resStateName = $(get-ScomAlertResolutionState -resolutionStateCode $resStateNumber).name | |
| $resStateName | |
| } | |
| function setResStateColor($resStateNumber) { | |
| switch($resStateNumber){ | |
| "0" { $sevColor = "FF0000" } #Color is Red | |
| "1" { $sevColor = "FF0000" } #Color is Red | |
| "255" { $sevColor = "3300CC" } #Color is Blue | |
| default { $sevColor = "FFF00" } #Color is Yellow | |
| } | |
| $sevColor | |
| } | |
| function stripCruft($cruft) { | |
| #Removes "cruft" data from messages. | |
| #Intended to make subject lines and alphanumeric pages easier to read | |
| $cruft = $cruft.replace("®","") | |
| $cruft = $cruft.replace("(R)","") | |
| $cruft = $cruft.replace("Microsoftr ","") | |
| $cruft = $cruft.replace("Microsoft ","") | |
| $cruft = $cruft.replace("Microsoft.","") | |
| $cruft = $cruft.replace("Windows ","") | |
| $cruft = $cruft.replace(" without Hyper-V","") | |
| $cruft = $cruft.replace("Serverr","Server") | |
| $cruft = $cruft.replace(" Standard","") | |
| $cruft = $cruft.replace(" Enterprise","") | |
| $cruft = $cruft.replace(" Edition","") | |
| $cruft = $cruft.replace(".campus","") | |
| $cruft = $cruft.replace(".CAMPUS","") | |
| $cruft = $cruft.replace(".ad.uvm.edu","") | |
| $cruft = $cruft.replace(".AD.UVM.EDU","") | |
| $cruft = $cruft.trim() | |
| return $cruft | |
| } | |
| function fnMamlToHTML($MAMLText){ | |
| $HTMLText = ""; | |
| $HTMLText = $MAMLText -replace ('xmlns:maml="http://schemas.microsoft.com/maml/2004/10"'); | |
| $HTMLText = $HTMLText -replace ("maml:para", "p"); | |
| $HTMLText = $HTMLText -replace ("maml:"); | |
| $HTMLText = $HTMLText -replace ("</section>"); | |
| $HTMLText = $HTMLText -replace ("<section>"); | |
| $HTMLText = $HTMLText -replace ("<section >"); | |
| $HTMLText = $HTMLText -replace ("<title>", "<h3>"); | |
| $HTMLText = $HTMLText -replace ("</title>", "</h3>"); | |
| $HTMLText = $HTMLText -replace ("<listitem>", "<li>"); | |
| $HTMLText = $HTMLText -replace ("</listitem>", "</li>"); | |
| $HTMLText; | |
| } | |
| function fnTrimHTML($HTMLText){ | |
| $TrimedText = ""; | |
| $TrimedText = $HTMLText -replace ("<", "<") | |
| $TrimedText = $TrimedText -replace (">", ">") | |
| $TrimedText = $TrimedText -replace ("<html>") | |
| $TrimedText = $TrimedText -replace ("<HTML>") | |
| $TrimedText = $TrimedText -replace ("</html>") | |
| $TrimedText = $TrimedText -replace ("</HTML>") | |
| $TrimedText = $TrimedText -replace ("<body>") | |
| $TrimedText = $TrimedText -replace ("<BODY>") | |
| $TrimedText = $TrimedText -replace ("</body>") | |
| $TrimedText = $TrimedText -replace ("</BODY>") | |
| $TrimedText = $TrimedText -replace ("<h1>", "<h3>") | |
| $TrimedText = $TrimedText -replace ("</h1>", "</h3>") | |
| $TrimedText = $TrimedText -replace ("<h2>", "<h3>") | |
| $TrimedText = $TrimedText -replace ("</h2>", "</h3>") | |
| $TrimedText = $TrimedText -replace ("<H1>", "<h3>") | |
| $TrimedText = $TrimedText -replace ("</H1>", "</h3>") | |
| $TrimedText = $TrimedText -replace ("<H2>", "<h3>") | |
| $TrimedText = $TrimedText -replace ("</H2>", "</h3>") | |
| $TrimedText; | |
| } | |
| function buildEmail { | |
| ## Format the message for full-HTML email | |
| [string] $escTxt = "" | |
| if ($resState -eq '1') {$escTxt = '- Repeat Count ' + $escLev.ToString()} | |
| [string] $script:mailSubj = "SCOM - $resStateName $escTxt - $alertSev | $moPath | $alertName" | |
| $mailSubj = stripCruft($mailSubj) | |
| [string] $script:mailErrSubj = "Error emailing SCOM Notification for Alert ID $alertID" | |
| [string] $webConsoleURL = $WebConsoleBaseURL+"?DisplayMode=Pivot&AlertID=%7b$alertID%7d" | |
| [string] $psCmd = "Get-SCOMAlert -Id `"$alertID`" | format-list *" | |
| # Format the Mail Message Body (do not indent this block!) | |
| $script:MailMessage.isBodyHtml = $true | |
| $script:mailBody = @" | |
| <html> | |
| <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> | |
| <body> | |
| <p><b>Alert Resolution State:<Font color='$sevColor'> $resStateName </Font></b><br/> | |
| <b>Alert Severity:<Font color='$sevColor'> $alertSev</Font></b><br/> | |
| <b>Object Source (Display Name):</b> $moSource <br/> | |
| <b>Object Path:</b> $moPath <br/> | |
| </p> | |
| <p> | |
| <p><b>Alert Name:</b> $alertName <br/> | |
| <b>Alert Description:</b> <br/> | |
| $alertDesc <br> | |
| "@ | |
| if (($resState -eq 0) -or ($resState -eq 1)) { | |
| if ($isMonitorAlert -eq $true) { | |
| $script:mailBody = $mailBody + @" | |
| <b>Alert Monitor Name:</b> $MonitorName <br/> | |
| <b>Alert Monitor Description:</b> $MonitorDescription | |
| </p> | |
| "@ | |
| }elseif ($isMonitorAlert -eq $false) { | |
| $script:mailBody = $mailBody + @" | |
| <b>Alert Rule Name:</b> $RuleName <br/> | |
| <b>Alert Rule Description:</b> $RuleDescription <br/> | |
| "@ | |
| } | |
| } | |
| $script:mailBody = $mailBody + @" | |
| <b>Alert Context Properties:</b><br/> | |
| $alertCX <br/> | |
| <b>Time Raised:</b> $timeRaised <br/> | |
| <b>Alert ID:</b> $alertID <br/> | |
| <b>Notification Status:</b> $($alert.CustomField1) </br> | |
| <b>Notification Repeat Count:</b> $($escLev.ToString()) </p> | |
| <p> | |
| <b>PowerShell Alert Retrieval:</b> $psCmd <br/> | |
| </p> | |
| "@ | |
| if (($resState -eq 0) -or ($resState -eq 1)) { | |
| foreach ($article in $arrArticles) { | |
| $articleContent = $article.content | |
| $script:mailBody = $mailBody + @" | |
| <p> | |
| <b>Knowledge Article / Company Knowledge `-$($article.Language):</b> | |
| <hr> | |
| <p> $articleContent | |
| <hr> | |
| <p> | |
| </body></html> | |
| "@ | |
| } | |
| } | |
| $script:mailErrBody = @" | |
| <html><body> | |
| <p>Error occurred when excuting script located at $RMS for alert ID $alertID. | |
| <p> | |
| <p>Alert Resolution State: $resStateName | |
| <p> | |
| <p>$error | |
| <p> | |
| <p><b>**Use below command to view the full details of this alert in SCOM Powershell console:</b> | |
| <p>$psCmd | |
| <p> | |
| <p> SCOM link:<a href = $webConsoleURL > $webConsoleURL </a> | |
| </body></html> | |
| "@ | |
| } | |
| function buildPage { | |
| ## Format the message for primitive alpha-numeric pager | |
| $script:moPath = stripCruft($moPath) | |
| [string] $escTxt = '' | |
| if ($resState -eq '1') {$escTxt = '- Rep Count ' +$escLev.ToString()} | |
| [string] $script:mailSubj = "SCOM - $resStateName $escTxt | $moPath" | |
| [string] $script:mailErrSubj = "Error emailing SCOM Notification for Alert ID $alertID" | |
| #UFT8 makes the message body look like trash. Use ASCII (the default) instead. | |
| #$mailMessage.BodyEncoding = [System.Text.Encoding]::UTF8 | |
| $script:MailMessage.isBodyHtml = $false | |
| $script:moSource = stripCruft($moSource) | |
| $script:alertName = stripCruft($alertName) | |
| $script:mailBody = "| $moSource | $alertName | $timeRaised" | |
| $script:mailBody = stripCruft($mailBody) | |
| } | |
| function buildHeaders { | |
| param( | |
| [array]$recips | |
| ) | |
| ## Complete the MailMessage object: | |
| $script:MailMessage.Sender = $Sender | |
| $script:MailMessage.From = $Sender | |
| $script:MailMessage.Headers.Add('references',$threadID) | |
| # Regular (non-error) format | |
| if ($error.count -eq "0") { | |
| $script:MailMessage.Subject = $mailSubj | |
| Foreach ($item in $recips) { | |
| $to = New-Object System.Net.Mail.MailAddress($item.email, $item.name) | |
| $script:MailMessage.To.add($to) | |
| Remove-Variable to | |
| } | |
| $script:MailMessage.Body = $mailBody | |
| } | |
| # Error format: | |
| else { | |
| $script:MailMessage.Subject = $mailErrSubj | |
| $script:MailMessage.To.add($ErrRecipient) | |
| $script:MailMessage.Body = $mailErrBody | |
| } | |
| ## Log the message if in diag mode: | |
| if ($diag -eq $true) { | |
| [string] $('Mail Message Object Content:') | Out-File $diagLogFile -Append | |
| $mailMessage | fl * | Out-File $diagLogFile -Append | |
| } | |
| } | |
| #### End Function Library #### | |
| #### Clean up existing logs: #### | |
| if (Test-Path $errorLogFile) {Remove-Item $errorLogFile -Force} | |
| if (Test-Path $diagLogFile) {Remove-Item $diagLogFile -Force} | |
| if ($diag -eq $true) { | |
| [string] $("AlertID : `t" + $alertID) | Out-File $diagLogFile -Append | |
| [string] $("MailTo : `t" + $mailto) | Out-File $diagLogFile -Append | |
| [string] $("PageTo : `t" + $pageto) | Out-File $diagLogFile -Append | |
| #[string] $("Format : `t" + $format) | Out-File $diagLogFile -Append | |
| } | |
| #### Begin Alert Handling: #### | |
| ## Locate the specific alert: | |
| $alert = Get-SCOMAlert -Id $alertID | |
| if ($diag -eq $true) { | |
| [string] $('SCOM Alert Object Content:') | Out-File $diagLogFile -Append | |
| $alert | fl | Out-File $diagLogFile -Append | |
| } | |
| ## Read Alert Informaiton: | |
| [string] $alertName = $alert.Name | |
| [string] $alertDesc = $alert.Description | |
| #[string] $alertPN = $alert.principalName | |
| [string] $moSource = $alert.monitoringObjectDisplayName # Display name is "Path" in OpsMgr Console. | |
| [string] $moId = $alert.monitoringObjectID.tostring() | |
| #[string] $moName = $alert.MonitoringObjectName # Formerly "strAgentName" | |
| [string] $moPath = $alert.MonitoringObjectPath # Formerly "pathName | |
| #[string] $moFullName = $alert.MonitoringObjectFullName # Formerly "alertFullName" | |
| [string] $ruleID = $alert.MonitoringRuleId.Tostring() | |
| [string] $resState = ($alert.resolutionstate).ToString() | |
| [string] $resStateName = getResStateName $resState | |
| [string] $alertSev = $alert.Severity.ToString() # Formerly "severity" | |
| if ($alertSev.ToLower() -match "error") { | |
| $alertSev = "Critical" # Rename Severity to "Critical" | |
| } | |
| [string] $sevColor = setResStateColor $resState # Assign color to alert severity | |
| #$problemID = $alert.ProblemId | |
| $alertCx = $([xml]($alert.Context)).DataItem.Property ` | |
| | Select-Object -Property Name,'#text' ` | |
| | ConvertTo-Html -Fragment # Alert Context property data, in HTML | |
| $localTimeRaised = ($alert.timeraised).tolocaltime() | |
| [string] $timeRaised = get-date $localTimeRaised -Format "MMM d, h:mm tt" | |
| [bool] $isMonitorAlert = $alert.IsMonitorAlert | |
| $escLev = 1 | |
| if ($alert.CustomField2) { | |
| [int] $escLev = $alert.CustomField2 | |
| } | |
| ## Lookup available Knowledge articles, if new alert: | |
| if (($resState -eq 0) -or ($resState -eq 1)) { | |
| $articles = $mg.Knowledge.GetKnowledgeArticles($ruleId) | |
| <# $articleContent = $null #> | |
| if (!$error) { #no point retrieving the monitoring rule when there's error processing the alert | |
| #if failed to get knowledge article, remove the error from $error because not every rule and monitor will have knowledge articles. | |
| if ($isMonitorAlert -eq $false) { | |
| $rule = Get-SCOMRule -Id $ruleID | |
| $ruleName = $rule.DisplayName | |
| $ruleDescription = $rule.Description | |
| if ($RuleDescription.Length -lt 1) {$RuleDescription = "None"} | |
| } elseif ($isMonitorAlert) { | |
| $monitor = Get-SCOMMonitor -Id $ruleID | |
| $monitorName = $monitor.DisplayName | |
| $monitorDescription = $monitor.Description | |
| if ($monitorDescription.Length -lt 1) {$monitorDescription = "None"} | |
| } | |
| #Convert Knowledge articles | |
| $arrArticles = @() | |
| Foreach ($article in $articles) { | |
| If ($article.Visible) { | |
| $LanguageCode = $article.LanguageCode | |
| #Retrieve and format article content | |
| $MamlText = $null | |
| $HtmlText = $null | |
| if ($article.MamlContent -ne $null) { | |
| $MamlText = $article.MamlContent | |
| $articleContent = fnMamlToHtml($MamlText) | |
| } | |
| if ($article.HtmlContent -ne $null) { | |
| $HtmlText = $article.HtmlContent | |
| $articleContent = fnTrimHTML($HtmlText) | |
| } | |
| $objArticle = New-Object psobject | |
| Add-Member -InputObject $objArticle -MemberType NoteProperty -Name Content -Value $articleContent | |
| Add-Member -InputObject $objArticle -MemberType NoteProperty -Name Language -Value $LanguageCode | |
| $arrArticles += $objArticle | |
| Remove-Variable LanguageCode, articleContent | |
| } | |
| } | |
| } | |
| if ($Articles -eq $null) { | |
| $articleContent = "No resolutions were found for this alert." | |
| } | |
| } | |
| ## End Knowledge Article Lookup | |
| #### End Alert Handling #### | |
| #### Begin Mail Processes: | |
| if ($mailto) { | |
| # For all alerts, send full HTML email: | |
| $MailMessage = New-Object System.Net.Mail.MailMessage | |
| buildEmail | |
| buildHeaders -recips $mailRecips | |
| invoke-command -ScriptBlock {$SMTPClient.Send($MailMessage)} -errorVariable smtpRet | |
| } | |
| if ($pageTo) { | |
| # For page-worthy alerts, format short message and send: | |
| $MailMessage = New-Object System.Net.Mail.MailMessage | |
| buildPage | |
| buildHeaders -recips $pageRecips | |
| invoke-command -ScriptBlock {$SMTPClient.Send($MailMessage)} -errorVariable smtpRet | |
| } | |
| #### End Mail Message Formatting #### | |
| # Populate CustomField1 and 2 to indicate that a notification has been sent, with repeat count. | |
| if (!$smtpRet) { # IF the message was sent (apparently)... | |
| [string] $updateReason = "Updated by Email notification script." | |
| [string] $custVal1 = "notified" | |
| if ($resState -eq "0") { # . AND IF this is a "new" alert... | |
| $alert.ResolutionState = 1 # ..Set the resolution state to "Notified" | |
| $alert.CustomField2 = $escLev # ..Set CustomField2 to the current notification retry count (presumably 1) | |
| if (!$alert.CustomField1) { # ..AND if CustomField1 is not already defined... | |
| $alert.CustomField1 = $custVal1 # ... Set CustomField1. | |
| } | |
| $alert.Update($updateReason) | |
| } | |
| elseif ($resState -eq "1") { # .Or,If this is a "notified" alert | |
| if ($alert.CustomField2) { # ..and the notification retry count exists.. | |
| $escLev += 1 # ...Increment by one. | |
| } | |
| $alert.CustomField2 = $escLev | |
| $alert.Update($updateReason) | |
| } | |
| } | |
| Write-Host $error | |
| ##Make sure the script is closed | |
| if ($error.count -ne "0") { | |
| [string]$('AlertID string: ' + $alertID) | Out-File $errorLogFile | |
| [string]$('Alert Object Content: ') | Out-File $errorLogFile | |
| $alert | Format-List * | Out-File $errorLogFile | |
| [string]$('Error Object contents:') | Out-File $errorLogFile | |
| $Error | Out-File $errorLogFile | |
| } | |
| Remove-Variable alert | |
| Remove-Module OperationsManager |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment