onsdag den 21. december 2011

ID3206: A SignInResponse message may only redirect within the current web application:

I got a question in a remark on ADFS 2.0 and ASP.NET and wanted to do a post about it instead of trying to fit in code inside a remark too.

Sometimes you might see the above error. And I can understand why it can be frustrating at times, so ill try and clarify what it means.

You create a sign message and redirect the user to the ADFS/STS. The user logs in successfully and gets send back with a token, and now WIF on the webserver say’s “go away, you are trying to login with a token issued for another domain/application than me.

The key here is your return URL. When you add a Relying Party on your ADFS server, you specify a WS-Federation Passive Endpoint. Your return URL need to be within same scope as your WS-Federation Endpoint URI.

I have added the code I’m using now, and added a few comments. Should clear things up a bit.

Code Snippet
  1. ' basicly this tell adfs, to redirect the user onward to some other STS. For most people this should not be specified.
  2. Dim HomeRealm As String = "urn:admin.wingu.dk"
  4. 'What Identity did we specify in the "Relying Party Trusts" on the ADFS server
  5. 'This has to also be listed in web.config under <microsoft.identityModel><service><audienceUris>
  6. ' or you will get ID1038: The AudienceRestrictionCondition was not valid because the specified Audience is not present in AudienceUris.
  7. ' This will most often be an URI, like https://admin.wingu.dk/SSI2 , but could also be an URN, like "urn:admin.website.ssi2"
  8. Dim Realm As String = ("https://admin.wingu.dk" & Request.ApplicationPath & "/").ToLower
  10. ' Lets inspect and modify the Return URL
  11. ' If this falls out of scope from what you specefied under WS-Federation Passive Endpoint, when adding you
  12. ' Relying Party on the ADFS server, you will get
  13. ' ID3206: A SignInResponse message may only redirect within the current web application:
  14. '
  15. ' Basicly, if you want several endpoints for you website. Like www.1.com and www.2.com or
  16. ' www.1.com/app1 and www.1.com/app1, add more Relying Party's, one for each.
  17. ' There is another way, maybe ill do a blog about that someday
  19. Dim ReturnURI As New UriBuilder(ReturnUrl.ToLower)
  20. ' We need to use HTTPS, no matter what, so
  21. ReturnURI.Scheme = "https"
  22. ReturnURI.Port = 443
  23. 'I dont want to redirect back to my self. This can be ok in some sceenarios
  24. If ReturnURI.Path.Contains("login.aspx") Then ReturnURI.Path = Request.ApplicationPath & "/"
  25. ' change localhost / computernamer or what ever, to the fqdn used on the ADFS to redirect back to
  26. ReturnURI.Host = "admin.wingu.dk"
  27. ReturnUrl = ReturnURI.ToString
  29. Dim authModule As WSFederationAuthenticationModule = FederatedAuthentication.WSFederationAuthenticationModule
  30. authModule.PassiveRedirectEnabled = True
  31. Dim mess As WSFederation.SignInRequestMessage = authModule.CreateSignInRequest("passive", ReturnUrl, False)
  33. mess.HomeRealm = HomeRealm
  34. mess.Realm = Realm
  35. Dim redirURL As String = mess.WriteQueryString()
  36. Response.Redirect(redirURL)

onsdag den 14. december 2011

SQL user does not use default schema

So I was troubleshooting an installation of SuperOffice Customer Service. When running the installer it would fail with an ODBC driver error saying something down the line of “’config’ object not found” and you could see it was doing a “select count(*) from config”

I jumped over to the SQL server and saw that the SuperOffice SQL user did not have crm7 schema set as default schema and was not owner. So I chanced that, and tried again. I would still failed. So I open a new query in Management studio and did a “select * from config” .. that should work, but didn’t, I would still get no such object.

after goggling a while I decided to do a alter user, so opened the “help” page for “alter user” and voila, after reading the page I suddenly knew what was wrong.

The value of DEFAULT_SCHEMA is ignored if the user is a member of the sysadmin fixed server role. All members of the sysadmin fixed server role have a default schema of dbo.

The person who had installed superoffice had added the SQL user crm7 to the sysadmin role, and as you can see, that will break the schema role assignment. After removing the user from sysadmin, everything worked again.

søndag den 23. oktober 2011

Deploy CRM List Component though PowerShell

as always I did a few searches on Google. Most hits just explained how to upload the WSP file and then call the AllowHtcExtn.ps1 script.

On blog did explain you could use Add-SPSolution and Install-SPSolution but that didn’t work for me.

$solution = Get-SPSolution crmlistcomponent.wsp
if(!$solution){ $solution = Add-SPSolution X:\SPS_2010_prov\listekomponent\en\crmlistcomponent.wsp }
-SPSolution -WebApplication "https://$webappurl" $solution -GACDeployment -Force

The solution didn’t show up in Solution Gallery inside SharePoint. And when I tested from CRM 2011 it kept complaining the List Component isn't installed.

I have just been to Microsoft Campus Days in Copenhagen ( kind of a “mini TechEd” ) and one of the session I was attending was about SharePoint Online and how Microsoft only allow Sandbox solutions, and then it hit me. Maybe the CRM list Component is a sandbox solution and needs to be deployed that way. We have a winner!

So we end up with

Write-Host "Starting Sharepoint User Code Service"
-o provisionservice -action start -servicetype Microsoft.SharePoint.Administration.SPUserCodeService -servicename SPUserCodeV4

-Host "Install crmlistcomponent if needed"
$solution = Get-SPUserSolution crmlistcomponent.wsp -Site "https://$webappurl" -ea 0
$temp = Add-SPUserSolution X:\SPS_2010_prov\listekomponent\en\crmlistcomponent.wsp
$solution = Get-SPUserSolution crmlistcomponent.wsp -Site "https://$webappurl"

-Host "Active crmlistcomponent if needed"
if($solution.Status -ne 'Activated'){
$solution = Install-SPUserSolution $solution -Site "https://$webappurl"

-Host "Make sure .htc file name extension is an allowed file type"
$app = Get-SPWebApplication -Identity "https://$webappurl"
-Host "Adding .htc file name extension to the list of allowed file types."

lørdag den 22. oktober 2011

Find owner of Fulltext catalog

Once in a while I would get this error when dropping SQL logins

The database principal owns a fulltext catalog in the database, and cannot be dropped.

Changing the owner is easy enough, but I’m doing this by scripts and needed a way to find the owner of the fulltext catalog. Took a while but finally managed to get the result with

select cat.fulltext_catalog_id, cat.name 
from sys.fulltext_catalogs as cat
inner join sys.database_principals as dp2
on cat.principal_id = dp2.principal_id

Perfect, so lets script that, and we get something like

$SQLLogin = 'soadmin_SOTEST2'
$sqlserver = 'sql01.int.wingu.dk'
$database = 'SO7_SO7SOTEST2'

$sql = "select cat.fulltext_catalog_id, cat.name from sys.fulltext_catalogs as cat "
$sql = ($sql + " inner join sys.database_principals as dp2 on cat.principal_id = dp2.principal_id ")
$sql = ($sql + "where dp2.name = '$SQLLogin'")

$rs = Invoke-Sqlcmd -ServerInstance $sqlserver -Database $database -Query $sql
foreach($row in $rs){
$sql = ("ALTER AUTHORIZATION ON Fulltext Catalog::[" + $row.Name + "] TO dbo")
-Sqlcmd -ServerInstance $sqlserver -Database $database -Query $sql

mandag den 10. oktober 2011

Working with SuperOffice though PowerShell

This powershell snap in is not meant to be a complete SuperOffice client, but for most basic stuff you can use this. Like say, you want to run a batch job everyday to update/sync information's from anther system and SuperOffice

working with powershell makes a lot of things a lot easier. Lets have a look at a few examples

First lets connect to SuperOffice. If you have the superoffice.config from your client laying around somewhere you can easily connect using that.

$module = Get-Module | where {$_.name -eq 'wingu.SuperOffice.SnapIn'}
if($module -eq $null){
$module = Get-Module -ListAvailable | where {$_.name -eq 'wingu.SuperOffice.SnapIn'}
if($module){ import-module wingu.SuperOffice.SnapIn }

$config = Get-SO7config c:\superoffice.config | Set-SO7config
$config.Username = $config.Explicit.DBUser
$config.Password = $config.Explicit.DBPassword

So, just to validate everything is working, lets get the license information
$LicenseInfo = Get-SO7LicenseInfo
# To replace running license use
Set-SO7LicenseInfo -CompanyName 'Happy Ducks Holding ApS' -SerialNumber '1234'
To Update current license. ( like if you buy more user licenses )
Set-SO7LicenseInfo $LicenseInfo

To see what credentials is associated with alz@sotest1.local and if he is logged on right now, you could so something like this

Get-SO7Person -email alz@sotest1.local | Get-SO7Credentials

Or to have a loot at who is using SuperOffice right now
Get-SO7Credentials | where {$_.CredentialType -eq 'Ticket' }

( this will not be accurate, since cleanup of ticket’s is only done when a user logs in. but still … )

Do you want a list of Persons asscociated with Contact ‘hestehøj a/s’ ?

Get-SO7Contact 'Hestejøj AS' | Get-SO7Person

Want to create a new Associate, with AD login ?

$logonName = 'alz'
$upn = 'alz@sotest1.local'
$FirstName = 'Allan'
$Lastname = 'Zimmermann'

# Get Associate if exists
$CRM7User = Get-SO7Associate -logonName $logonName -ea 0
# Get Person with email corrosonding to upn, if exists
$CRM7Person = Get-SO7Person -email $upn -ea 0
if((!$CRM7User) -and (!$CRM7Person)){
# could not find a person nor Associate so create both
$CRM7User = New-SO7Associate -FirstName $FirstName -Lastname $Lastname -logonName $logonName -Email $upn -AdUser
$CRM7User = Get-SO7Associate -logonName $logonName -ea 0
$CRM7Person = Get-SO7Person -email $upn -ea 0
if((!$CRM7User) -and ($CRM7Person)){
# Person exists, but have no Associate so create it and link to person
$CRM7User = New-SO7Associate -person $CRM7Person -logonName $logonName -Email $upn -AdUser
$CRM7User = Get-SO7Associate -logonName $logonName -ea 0
# Assign a UserLicense, allow login, and enable Windows/web
$CRM7User | Enable-SO7Associate

Or, create with a CRM5 password (not ad login) ?
$logonName = 'alz'
$upn = 'alz@sotest1.local'
$FirstName = 'Allan'
$Lastname = 'Zimmermann'
$Password = 'Passw0rd'

# Get Associate if exists
$CRM7User = Get-SO7Associate -logonName $logonName -ea 0
# Get Person with email corrosonding to upn, if exists
$CRM7Person = Get-SO7Person -email $upn -ea 0
if((!$CRM7User) -and (!$CRM7Person)){
# could not find a person nor Associate so create both
$CRM7User = New-SO7Associate -FirstName $FirstName -Lastname $Lastname -logonName $logonName -Email $upn -Password $Password
$CRM7User = Get-SO7Associate -logonName $logonName -ea 0
$CRM7Person = Get-SO7Person -email $upn -ea 0
if((!$CRM7User) -and ($CRM7Person)){
# Person exists, but have no Associate so create it and link to person
$CRM7User = New-SO7Associate -person $CRM7Person -logonName $logonName -Email $upn -Password $Password
$CRM7User = Get-SO7Associate -logonName $logonName -ea 0
# Assign a UserLicense, allow login, and enable Windows/web
$CRM7User | Enable-SO7Associate

anyway, here is a complete list of commands.
Get-Command -Module wingu.SuperOffice.SnapIn


lørdag den 8. oktober 2011

SharePoint and WebDAV–part 2

So I was troubleshooting mapping SharePoint though WebDAV and found I had to open the SharePoint site though office in one way or the other, in order to successfully map it. That was true, but for one very specific reason.

Talking about cookies, you need to know there is 2 kinds of cookies. Session Cookies and Persistent cookies. Session cookies only “lives” as long as your browser is open. As soon as you close *ALL* your browser windows they cookie will expire. Persistent cookies will stay on your machine until the expire date on the cookie is hit.

When you setup SharePoint to use Claimsbased authentication SharePoint save a cookie on your machine called the “FedAuth” cookie. This is your what authenticate you every time you hit SharePoint. It gets set when the Identity Provider (STS/ADFS) makes your browser “post” your claims to https://fqdn/_trust/ .It gets removed when you click “sign out” or “Sign in as Different User”.

The WebDAV client, and various Office applications does support working with websites that facilitate Claimsbased authentication, but they cannot authenticate you. It assumes you have a FedAuth cookie on your machine. Some times you will get a browser popup, but there are places this wont happen (like with WebDAV). I had told SharePoint to use Session Cookies, since that would make more sense when working with SharePoint. You login you do your stuff, you might sign out, or you might close your browser. Next time you hit it, it asks you to login. I was happy, everyone else was happy. That is, until someone needed to work with SharePoint from within Outlook and though WebDAV.

SharePoint will standardly run with Persistent Cookies, but you can change this though PowerShell. Set UseSessionCookies to $true or $false. If you need to work with SharePoint though Office or WebDAV you need to set this to $false (default)

$snapin = Get-PSSnapin | where {$_.name -eq 'Microsoft.SharePoint.PowerShell'}
if($snapin -eq $null){ Add-PSSnapin Microsoft.SharePoint.PowerShell }

$sts = Get-SPSecurityTokenServiceConfig
$sts.UseSessionCookies = $false

As far as I know, you cannot set this per site.

Next you want to make sure Office/Web Client can read the cookie. Referee to “Persistent cookies are not shared between Internet Explorer and Office applications

onsdag den 5. oktober 2011

First encounter with SuperOffice 7 PocketCRM

Apparently some people inside the company I work for, have told some one else that I have had a bit of experience with SuperOffice. One of our SuperOffice consultants was troubleshooting a failed SuperOffice 7 PocketCRM installation, and needed some help getting it up and running. I have never seen that product but I never back down from a good challenge.

The first thing I noticed when logging on to the server, was that SuperOffice web was installed, and didn’t work. If you browsed the local website, no matter what you wrote you would just get an HTTP 404 file not found. The weird thing was the error came from a Tomcat server and not IIS. I did a net stat –ano to see if Tomcat had “stolen” port 80, but that was not the case. After looking around a bit inside IIS, I noticed there was added an ISAPI filter pointing to a DLL inside the tomcat directory. After removing that entry, IIS worked again, and we could login to SuperOffice web.

But now Pocket CRM wasn’t working. Knowing that the ISAPI DLL was “eating” all web request, I  decided to created a new website and added the ISAPI filter to that website, and I also added a virtual directory pointing to Jakarta as it had been doing on the original website. browsing to the new website’s /dl I would now be presented with the option to download the pocket CRM client. We tested if we could login from a client, but had no luck. After several different test’s we managed to get an error from the client saying it couldn’t connect to http://fqdn/NetServer/soPrincipal . I ran the installer for SoWeb and created a new NetServer installation on the main website (the one with SoWeb) with a virtual directory called /NetServer. Ran config and we tested the CRM Pocket client again.

Now we would just get login failed, no matter what. I enabled logging on the NetServer installation, but it wouldn’t generate any log.

<add key="LogError" value="True" />
<add key="LogWarning" value="True" />
<add key="LogFailureAudit" value="True" />
<add key="LogSuccessAudit" value="True" />
<add key="LogInformation" value="False" />
<add key="LogToFile" value="True" />
<add key="LogFolder" value="c:\temp" />

Weird. I opened visual studio on my own machine, added a reference to the above server and created a small test application
    Sub Main()
Dim WS As New ServiceReference1.SoPrincipalClient
Dim Password As String = "secret", UserName As String = "user"
Dim Succeeded As Boolean, TimeZone As ServiceReference1.SoTimeZone, Response As ServiceReference1.SoPrincipalCarrier
Dim AuthenticationSucceeded
Dim Credentials As ServiceReference1.SoCredentials
Dim WsClient As test.ServiceReference1.SoExceptionInfo = WS.AuthenticateUsernamePassword(Password, UserName, _
Succeeded, TimeZone, Response, AuthenticationSucceeded, Credentials)
"Succeeded: " & Succeeded)
"AuthenticationSucceeded: " & AuthenticationSucceeded)
If Credentials Is Nothing Then
"Credentials is nothing")
"Credentials Ticket: " & Credentials.Ticket)
End If
Dim s As String = ""
End Sub

I cant re-generate the error message, to show here on the blog, but it said something about about not being able to bind the web service to port “http”. I have seen that error before so I opened web.config and added

<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />

After that I could now successfully authenticate to the NetServer, so we went back to the SuperOffice client. Same stuff, Client would say “authentication failed” and NetServer was net generating any log.

I went back to my application and wrote a wrong username and password. a log file was created. I delete the logs, chanced username/password to some valid credentials and ran the application again. no log file got generated … So seems SuperOffice has a bug and isn't logging successful logins when told too. We did the same test from the PocketCRM client, and saw the same thing happen. It would log when typing wrong username and password, and no log when we typed valid credentials. But the client kept saying “authentication failed”

After enabling debug logging inside tomcat I noticed PocketCRM wasn’t talking with NetServer though Tomcat, so I had the weird idea that maybe the PocketCRM application inside Tomcat didn’t talk with the correct NetServer

After looking though different files, I found \SuperOffice\SuperOffice 7 Server\Modules\PocketCRM\Tomcat 6.0\webapps\#pocketcrm\WEB-INF\classes and saw the NetServer URL was wrong. I changed it, and restarted PocketCRM, and voila. Everything worked.

søndag den 2. oktober 2011

Managing CRM 2011 with powershell

New updated version to better handle kerberos authentication, support for CRM online, and CRM 2011 and 2013. read more here
I see a lot of hits from search engines where people are searching for stuff related to Microsoft CRM 2011 and powershell. So I decided to upload the snap in a wrote about, a few months ago.

The problem with CRM 2011 is that the powershell commands that comes with CRM 2011 only works on the local machine. but even worse, you can do anything from the commands except doing basic configuration of the installation it self. obvious stuff like adding users and changing roles isn't supported.

So here is a PowerShell Snap in (works as module too) that does exactly all that. It works on any machine that have .NET 4.0 and powershell 2.0 installed. Just copy the \debug folder to C:\Windows\System32\WindowsPowerShell\v1.0\Modules\CRM2011.Module open powershell and type “import-Module CRM2011.Module” and your ready to rock.

  • Add-CRMUserRole
  • Disable-CRMSystemUser
  • Enable-CRMSystemUser
  • Get-CrmLanguage
  • Get-CrmOrganization
  • Get-CrmOrganizationLanguage
  • Get-CrmRole
  • Get-CRMSystemUser
  • Get-CRMSystemUserSettings
  • Get-CRMUserRole
  • New-CRMCustomerAdminRole
  • New-CrmOrganization
  • New-CrmOrganizationLanguage
  • New-CRMSystemUser
  • Remove-CrmOrganization
  • Remove-CrmOrganizationLanguage
  • Set-CrmOrganization
  • Set-CRMSystemUser
  • Set-CRMSystemUserSettings
  • Update-CRMSystemUser

lørdag den 1. oktober 2011

Getting output from process spawned from powershell

Updated 02-10-2011: Fixed a small bug that could make SmartProcess consume 100%
There is several ways you can spawn a new process from within PowerShell. Start-Process for instance. If you create a new object of type System.Diagnostics.Process or call Start-Process and get such an object back, you can now get access to the Standard Input/Standard Output … there is a “catch” thou. These only get updated every time the process sends a complete line including a newline.

So lets say you spawn nslookup and want to catch every time i t expects input, it wont work.

function StartProcess([String]$FileName){
$process = New-Object "System.Diagnostics.Process"
$startinfo = New-Object "System.Diagnostics.ProcessStartInfo"
$startinfo.FileName = $FileName
#$startinfo.Arguments = $arguments
#$startinfo.WorkingDirectory = $pwd.Path
$startinfo.UseShellExecute = $false
$startinfo.RedirectStandardInput = $true
$startinfo.RedirectStandardOutput = $true
$startinfo.RedirectStandardError = $false
#$startinfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
$startinfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Normal

$process.StartInfo = $startinfo
$temp = $process.start()
return $process

function GetPrompt([System.Diagnostics.Process]$process, [Int]$waitSec){
$str = ''
$i = 1
while( (!([String]$str).contains("> ")) -and (!$process.HasExited) -and ($i -le $waitSec) -and ($process.StandardOutput.Peek() -eq -1) ){
# Write-Host ("Peek: " + $process.StandardOutput.Peek())
while($process.StandardOutput.Peek() -ge 0){
$iChar = $process.StandardOutput.Read()
$str = ($str + [Convert]::ToChar($iChar))
if(!([String]$str).contains("> ")){ Write-Host "."; Start-Sleep -s 1; }
$i = $i + 1
return $str

#[void] [System.Reflection.Assembly]::LoadWithPartialName("'System.Windows.Forms")

if(!$process.HasExited){ Write-Host "Killing process"; $process.kill(); $process.StandardOutput.ReadToEnd() }
$process = StartProcess 'nslookup'
$process 2
$process.StandardInput.WriteLine('set q=MX')
$process 2
GetPrompt $process 2

if(!$process.HasExited){ $process.kill() }

So you could write some code and hook up to the ErrorDataReceived and OutputDataReceived events. but those have the same problem, so you need to dig a bit deeper. You need to take each stream object and hook up to BaseStream.BeginRead

So that is what I have done. Inside my SuperOffice Add in I have added a new command called New-SmartProcess

And now the above code will look cleaner and actually work

function WaitForPrompt([wingu.SmartProcess]$process, [Int]$waitSec){
$str = ''
$i = 1
while( (!($process.StandardOutput.contains("> ")) -and (!$process.HasExited) -and ($i -le $waitSec))){
if(!([String]$str).contains("> ")){ Write-Host "."; Start-Sleep -s 1; }
$i = $i + 1
return $str

if($process){ if(!$process.HasExited){ Write-Host "Killing process"; $process.kill(); $process.StandardOutput.ReadToEnd() } }
$process = New-SmartProcess 'nslookup'
$process 2
$process.StandardOutput = ''
$process.WriteLine('set q=MX')
$process 2
$process.StandardOutput = ''
$process 2
$process.StandardOutput = ''
if(!$process.HasExited){ $process.kill() }

You can find my SuperOffice PowerShell snapin inclucing Source code here

fredag den 30. september 2011

Installshield MSI Multiple Instances

While working on automating installation of SuperOffice Customer Service (formerly known as eJournal ) I ran into a few issues. I started with just copying all files and changing configuration files but it didn’t quite do the trick. Doing installation of Customer Service it also install's 2 Windows Services and scripting that would just not be worth it, so I decided to see if I could control the installer instead.

If you have installed SuperOffice Customer Service once, the next time you call SuperOffice.CustomerService.exe you will get a prompt to either create a new instance or modify an existing installation


I’m no expert on MSI installers, but I did some digging around and it seems the way they get that to work, is doing some Transform scripting inside the installer. (that’s why it is an exe and not an MSI )

The first thing I did was extract the MSI and have a look at the properties in the installer so I could pass on the correct parameters. but when I called MSIEXEC with the SMI, I would get all kinds of different errors. (makes sense now ). After a lot of goggling I found that I could control the instance by number, by adding /Instance= to the .EXE … test that I created a couple in instances like this

SuperOffice.CustomerService.exe /instance=1 /V"EJHOSTNAME=servicesotest1.so7.wingu.dk INSTALLDIR=C:\servicesotest1.so7.wingu.dk /L*v c:\1.log /passive"
SuperOffice.CustomerService.exe /instance=2 /V"EJHOSTNAME=servicesotest2.so7.wingu.dk INSTALLDIR=C:\servicesotest2.so7.wingu.dk /L*v c:\2.log /passive"

And uninstalling

SuperOffice.CustomerService.exe /x /instance=1 /V" /L*v c:\1.log /passive"
SuperOffice.CustomerService.exe /x /instance=2 /V" /L*v c:\2.log /passive"

so I though that would be awesome, I could just pass on my CustomerService ID as Instance Name and everything would be perfect. But no … if I pass on a number higher than 1000 I fails and doesn’t work, so I had to do some more digging. I decided to see if I could emulate all the instance numbers and find a “free” instance number when installing, and the same, when uninstalling and look at install location.

I came across this neat little PowerShell Snap In/Module to get information about installed software. but you can also get all the information with WMI if you prefer that kind of stuff

$programs_installed = @{};
$win32_product = @(get-wmiobject -class 'Win32_Product' )
foreach ($product in $win32_product) {
$product | fl
# $product.InstallLocation
$name = $product.Name;

$version = $product.Version;
if ($name -ne $null) {
$programs_installed.$name = $version;

So now I can easely get a list of installed instances including my “instance number” by running

Get-MSIProductInfo -Name 'SuperOffice Customer Service' | fl ProductName, ProductCode, InstallLocation, PackageCode, InstanceType, Transforms

For a complete list, do

Get-MSIProductInfo | fl AuthorizedLUAApp, InstallContext, InstalledProductName, Language, Name, PackageCode, PackageName, ProductIcon, Transforms, Version, CollectUserInfo, Equals, GetComponentState, GetFeatureState, GetHashCode, GetType, ToString, PSPath, Item, AdvertisedLanguage, AdvertisedPackageCode, AdvertisedPackageName, AdvertisedPerMachine, AdvertisedProductIcon, AdvertisedProductName, AdvertisedTransforms, AdvertisedVersion, Context, Features, HelpLink, HelpTelephone, InstallDate, InstallLocation, InstallSource, IsAdvertised, IsElevated, IsInstalled, LocalPackage, PrivilegedPatchingAuthorized, ProductCode, ProductId, ProductName, ProductVersion, Publisher, RegCompany, RegOwner, SourceList, UrlInfoAbout, UrlUpdateInfo, UserSid, Advertised, Installed, V1, AssignmentType, DiskPrompt, InstanceType, LastUsedSource, LastUsedType, MediaPackagePath, ProductState, VersionMajor, VersionMinor, VersionString

So to wrap it all up ….

function InstallSuperOfficeCustomerService([String]$fqdn){
$InstanceID = 0
#$AllreadyInstalled = Get-MSIProductInfo -Name 'SuperOffice Customer Service'
$AllreadyInstalled = @(get-wmiobject -class 'Win32_Product' ) | where {$_.Name -eq 'SuperOffice Customer Service'}
# add 1, to avoid getting "default" instance, and check if its free
$InstanceID = $InstanceID + 1
$isFree = $AllreadyInstalled | where { $_.Transforms -eq ":InstanceId$InstanceID.mst" }
# If not free, loop til we find one unused
$InstanceID = $InstanceID + 1
$isFree = $AllreadyInstalled | where { $_.Transforms -eq ":InstanceId$InstanceID.mst" }
if(! (Test-Path 'c:\CustomerService')){ $temp = New-Item 'c:\CustomerService' -type directory }
# replace /passive with /qn to hide the UI
$parameters = "/instance=$InstanceID /V""EJHOSTNAME=$fqdn INSTALLDIR=C:\CustomerService\$fqdn /L*v c:\CustomerService\install-$fqdn.log /passive"""
# Write-Host ("X:\SuperOfficeInstall\Customer Service 7.0 SR2\SuperOffice.CustomerService.exe " + $parameters)
$installStatement = [System.Diagnostics.Process]::Start( "X:\SuperOfficeInstall\Customer Service 7.0 SR2\SuperOffice.CustomerService.exe", $parameters )

function UnInstallSuperOfficeCustomerService([String]$fqdn){
# now i could look though the instance number and call it with
# SuperOffice.CustomerService.exe /x /instance=$InstanceID /V" /L*v c:\$InstanceID.log /passive"
# but instead i'll play it safe and use the ProductCode

#$isInstalled = Get-MSIProductInfo -Name 'SuperOffice Customer Service' | where {$_.InstallLocation -like "*$fqdn*" }
$isInstalled = @(get-wmiobject -class 'Win32_Product' ) | where {($_.Name -eq 'SuperOffice Customer Service') -and ($_.InstallLocation -like "*$fqdn*")}
# replace /passive with /qn to hide the UI
# $ProductCode = $isInstalled.ProductCode
$ProductCode = $isInstalled.IdentifyingNumber
$parameters = "/x $ProductCode /passive /L*v c:\CustomerService\uninstall-$fqdn.log "
# Write-Host ("msiexec " + $parameters)
$uninstallStatement = [System.Diagnostics.Process]::Start( "msiexec", $parameters )

function IsSuperOfficeCustomerServiceInstalled([String]$fqdn){
$isInstalled = @(get-wmiobject -class 'Win32_Product' ) | where {($_.Name -eq 'SuperOffice Customer Service') -and ($_.InstallLocation -like "*$fqdn*")}
return $true
else {
return $false

InstallSuperOfficeCustomerService 'deleteme4.domain.com'

UnInstallSuperOfficeCustomerService 'deleteme4.domain.com'

IsSuperOfficeCustomerServiceInstalled 'deleteme4.domain.com'
IsSuperOfficeCustomerServiceInstalled 'deleteme5.domain.com'