Monday, December 7, 2015

Find everywhere a Column or Site Column is used

Ever needed to remove a Site Column that had at one point been published with a Content Type from the Content Type Hub?  This can be a real pain since there is no easy way to find everywhere that Site Column had ever been used, even where it has been directly added to a list or library that doesn't use the Content Type.  That's just one scenario where the PowerShell script below can come in handy. Once found it may also be helpful to quickly delete the column everywhere it is used or selectively in some places.

The script below does all that. Here is an overview first:
  • Iterates over all lists and libraries in all Webs in all Site Collections in a SharePoint Online tenant (excluding the Admin Portal and My Sites) to find a column that matches the SCName variable. If found it outputs the details of that occurrence of the column.  If the DeleteColumns variable is set to $true the column will automatically be deleted or in the case where the PromptBeforeDelete variable is also set to $true then you are prompted to optionally delete the column where it was found.
  • Input variables:
    • $script:SCName: string representing a Site Column's display name, internal name OR GUID
    • $tenantUrl: Tenant's root SharePoint site URL (a string)
    • $AdminUrl: Tenant's Admin Portal URL (a string)
    • $script:Debug: show debug or other non-critical error messages ($false or $true)
    • $script:DeleteColumns: delete the column from lists or libraries ($false or $true)
    • $script:PromptBeforeDelete: ask whether to delete the column each time it is found ($false or $true)
    • $user: account used to log in to the tenant admin portal and all site collections
    • $spocmdletsPath: local file system path to the folder where the spocmdlets were installed
    • $loadcsompropertiesPath: local file system path to the folder where the loadl-csomproperties script is located
  • Output: list of SharePoint lists or libraries where a column was found that matched the input string. Output list returns the following details for each column found:
    • Site URL
    • List/Library Name
    • Column's display name
    • Column's internal name
    • Column's GUID
  • Requirements/Dependencies
  • Future enhancements
    • Output to CSV
    • Create as a function with parameters or parameterized ps1
    • Download and load dependencies on the fly as needed
Script:

 #Configurable Settings  
 $user = "adminuser@tenanturl.com" #This user will be used to connect to site and create library. Pwd will be prompted for during execution.  
 $spocmdletsPath = "C:\downloads\SPO-Powershell\SPOAutomationCmdlets\SPOCmdlets" #  
 $loadcsompropertiesPath = "C:\downloads\SPScripts\Load-CSOMProperties.ps1"  
 $AdminUrl = "https://tenant-admin.sharepoint.com/"  
 $tenantUrl ="https://tenant.sharepoint.com"  
 $script:SCName = "Coffee"  
 $script:Debug = $false  
 $script:DeleteColumns = $false  
 $script:PromptBeforeDelete = $true  
   
 #Import-Module SPOCmdlets needed for Get-SPOContext, New-SPOList & Add-SPOListCT cmdlets  
 Import-Module $spocmdletsPath  
 Add-Type –Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"   
 Add-Type –Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"  
 $loadcsompropertiesPath  
   
   
 function ConvertFrom-SecureToPlain {  
     
   param( [Parameter(Mandatory=$true)][System.Security.SecureString] $SecurePassword)  
     
   # Create a "password pointer"  
   $PasswordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)  
     
   # Get the plain text version of the password  
   $PlainTextPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($PasswordPointer)  
     
   # Free the pointer  
   [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($PasswordPointer)  
     
   # Return the plain text password  
   #[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($PlainTextPassword))  
   $PlainTextPassword  
 }  
   
   
 $SecureString = $null  
 $r = $null  
   
 $SecureString = Read-Host -AsSecureString 'Enter password for'$user  
 $r = ConvertFrom-SecureToPlain($SecureString)  
 $SecurePassword = $r | ConvertTo-SecureString -AsPlainText -Force  
 $script:Credentials = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist $user, $SecurePassword  
   
   
 Connect-SPOService -Url $AdminUrl -Credential $Credentials  
   
 #Get all non admin or My or Public site collections  
 $sites = Get-SPOSite | where {$_.url -Match "$tenantUrl/*"}  
 Disconnect-SPOService  
   
 $:Credentials = New-Object -TypeName Microsoft.SharePoint.Client.SharePointOnlineCredentials -argumentlist $user, $SecurePassword  
 [System.Collections.ArrayList]$libraries = New-Object System.Collections.ArrayList  
   
 function script:GetListsThatUseSC($web,$libraries){  
     
   $Context = New-Object Microsoft.SharePoint.Client.ClientContext($web.url)  
   $Context.Credentials = $Credentials   
   $context.RequestTimeOut = 5000 * 60 * 10;  
   $webI = $Context.Web  
   #$context.Load($webI)  
   Load-CSOMProperties -object $webI -propertyNames @("Title", "Url", "Webs")  
   $Lists = $webI.Lists  
   $Context.Load($Lists)  
   try {   
     $Context.ExecuteQuery()   
     #Load-CSOMProperties -object $Lists -propertyNames @("AllProperties", "Url", "Title")  
     Load-CSOMProperties -parentObject $webI -collectionObject $webI.Lists -propertyNames @("Id", "Title", "Fields") -parentPropertyName "Lists"   
     $Context.ExecuteQuery()   
     if($Debug)  
     {  
       Write-Progress -Activity $webI.url -PercentComplete 100 #"("$webI.Lists.Count "lists)" #-foregroundcolor black -backgroundcolor green -NoNewLine  
         
     }  
     #iterate over each list/library  
     [int]$listIndex = 1  
     [int]$listTotal = [convert]::ToInt32($Lists.Count,10)  
     foreach( $list in $Lists )  
     {  
       [int]$percComplete = ($listIndex/$listTotal)*100  
         
   
       Write-Progress -Activity $webI.url -PercentComplete $percComplete  
       #$libraries.Add($list.Title)  
       foreach($field in $list.Fields)  
       {  
         if (($field.Title -eq $SCName) -or ($field.InternalName -eq $SCName) -or ($field.Id -eq $SCName))  
         {  
           Write-Host $web.Url">"$list.Title":"$field.Title","$field.Id","$field.InternalName #-foregroundcolor black -backgroundcolor green   
           #Delete the column?  
           if($DeleteColumns)  
           {  
             if($PromptBeforeDelete){  
               $d = Read-Host "Delete column? [Y]es or [N]o"  
               if($d.ToLower() -eq "y")  
               {  
                 $Field.DeleteObject()  
                 $Context.ExecuteQuery()  
               }  
             }else  
             {  
               $Field.DeleteObject()  
               $Context.ExecuteQuery()  
             }  
           }  
         }  
       }  
       $listIndex = $listIndex+1  
     }  
   }  
   catch {   
     if($Debug)  
     {  
       Write-Host "Not able to authenticate to SharePoint Online $_.Exception.Message" -foregroundcolor black -backgroundcolor Red  
     }  
   }   
     
  #iterate over all columns  
   
  #log library and lists/library URL where site column is used  
     
  #iterate over each subweb  
  try{  
     if($webI.Webs.Count > 0)  
     {  
       foreach($sub in $webI.Webs)  
       {  
         GetListsThatUseSC($sub, $libraries)  
       }  
     }  
  }catch{  
     if($Debug)  
     {  
       Write-Host "Subwebs not found or could not be loaded: $_.Exception.Message" -foregroundcolor black -backgroundcolor Red  
     }  
  }  
   return $libraries  
   
 }     
   
 #iterate of each Site Collection   
 foreach ($site in $sites)  
 {  
   GetListsThatUseSC($site, $libraries)  
 }  
   
 #$libraries  
   
 #clear pwd  
 $r = $null  

Monday, August 24, 2015

Configure SMTP Service to Queue Outgoing Emails for Testing

Testing SharePoint Designer workflows that send emails can be a pain for developers because the SharePoint farm's outgoing email settings needs to be properly configured (usually in a development environment by using the Windows Server SMTP service locally on the farm) yet we don't actually want emails to get sent to real end users while testing the workflow.
 
One option is to turn off the local SMTP service the SharePoint farm is configured to leverage for sending emails. The problem with this option is the workflow will log the error "The e-mail message cannot be sent. Make sure the outgoing e-mail settings for the server are configured correctly."  Yes, this does prevent users from getting spammed but it also prevents the workflow from continuing on to other steps and doesn't give you, the workflow developer, any means of evaluating the contents of the email.
 
Here is a better option. The premise with this option is that we setup the development SharePoint farm to use a local SMTP service which won't affect anyone else but we misconfigure the SMTP service so that it isn't actually able to transmit outbound emails. Instead it only queues the outbound emails as individual files on the file system. This gives us the ability to see when each email is "sent", though unsuccessfully, and also allows us to examine the contents of each email so that we can evaluate whether the workflow sent the emails as expected.  The assumption here is that you have already added the SMTP role to the server.
 
Here are the steps:
  1. Open IIS 6.0 on SMTP server
  2. Right click on SMTP node and choose Properties
  3. On the Access tab:
    1. Click Relay button
    2. Choose the "Only the list below" option
    3. Add IP of all SharePoint servers that should be allowed to send emails through this SMTP server
  4. On the Delivery tab:
    1. Click Advanced button
    2. Enter [0.0.0.0] in the Smart Host field (include the square brackets or you'll get an error that it can't resolve the name)
    3. Make sure "Attempt direct delivery before sending to the smart host" option is unchecked
  5. Test SMTP service
    1. On the SMTP server or other computer with the Telnet client installed send a test email using these instructions: http://technet.microsoft.com/en-us/library/bb123686(v=exchg.150).aspx (Step 3)
    2. On the SMTP server, open Explorer to the default SMTP queue folder, typically located at C:\inetpub\mailroot\Queue.  All emails should be saved as text files in this folder. You can open them using Notepad or using Outlook so you don't have to parse the text version of the email visually.