Wednesday, 25 June 2014

Script Creating SharePoint User MySites using PowerShell

While working on a large migration project (SharePoint 2010 to SharePoint 2013), I had a requirement to script the creation of user MySites for hundreds of users. I knocked this little PowerShell function together to do just that!

Function Create-MySite            
{            
 [CmdletBinding()]            
 Param            
 (            
  [Parameter(Mandatory = $True,Position=2,valueFromPipeline=$true)][String]$Username,            
     [Parameter(Mandatory = $True,Position=1)][String]$MySiteRootURL              
 )            
 [void][reflection.assembly]::Loadwithpartialname("Microsoft.Office.Server");               
 $site=new-object Microsoft.SharePoint.SPSite($MySiteRootURL);            
 try            
 {            
  $serviceContext = Get-SPServiceContext $site;            
  $upm = new-object Microsoft.Office.Server.UserProfiles.UserProfileManager($serviceContext);             
  if($upm.UserExists($Username) -eq $false)            
  {            
   Write-Host "User $Username was not found in the profile store." -f yellow;            
   return;            
  }            
  $userProfile = $upm.GetUserProfile($Username);            
  if($userProfile.PersonalSite -eq $Null)            
  {            
   Write-Host "Creating MySite for user $Username" -f darkyellow;            
   $userProfile.CreatePersonalSite();                  
   Write-host "Successfully created MySite for user $Username" -f green;            
  }            
  else            
  {            
   Write-Host "User $Username already has a MySite." -f darkgreen;            
  }            
 }            
 catch            
 {            
  Write-Host "Encountered an error creating a MySite for user $Username. Error:"$_.Exception -f Red;            
 }            
 finally            
 {            
  $site.Dispose();            
 }            
}

You can call the function like this (for a single user):

#Example - Create a MySite for user  "tonyj"            
Create-MySite -MySiteRootURL "http://mysite.bigintranet.com.au" -Username "tonyj"

Or call it like this, for a batch of users:

#To Create for an array of users            
$users = @('tonyj','bobh','markf','billd')            
$users | Foreach {Create-MySite -MySiteRootURL "http://mysite.bigintranet.com.au" -username $_}

Monday, 16 June 2014

Recursively Disabling a SharePoint Feature throughout a Farm

I'm working on a large SharePoint migration project at the moment. It's required quite a bit of PowerShell to automate tasks that ensure the project is a success.

One of those tasks was to find everywhere a feature was activated through the farm, and optionally disable the feature. Whenever we disable a feature throughout the farm, we need to keep a report of all the places the feature was previously enabled (there's about ten thousand webs in this farm).

So I wrote a script, that does exactly that! The script takes a feature-id, an SPWeb and two switches as parameters, and returns an object collection that records details about the feature instances disabled (feature id and display name, the web url, the feature status and the time it was last activated).

The two parameters, -Recurse, and -Report, control the scripts function. The -Recurse parameter tells the script to check all of the input webs sub-webs. The -ReportOnly parameter tells the script to... well, create a report of everywhere the feature is currently active, without actually disabling it!

Here's the function:

function Outst-SPFeature            
{            
 [CmdletBinding()]                        
    Param(                          
            [parameter(Mandatory=$true,Position = 0,valueFromPipeline=$true)][Microsoft.SharePoint.SPWeb]$Web,            
            [parameter(Mandatory=$true)][string]$FeatureId,            
            [parameter(Mandatory=$false)][switch]$Recurse,            
   [parameter(Mandatory=$false)][switch]$ReportOnly            
        )            
 #Define an object that can store the features properties             
 $FeatureInfo = New-Object psobject            
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "Id" -value ""
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "DisplayName" -value ""
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "WebUrl" -value ""
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "Status" -value ""
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "TimeActivated" -value ""             
 #create an empty array variable            
 $matches = @();            
             
 Write-Host "Checking web"$Web.Url -f Green -b Yellow;#pipe the list of features ($web.Features) to the Where-Object cmdlet (aliased as '?'), and look for a feature with the same FeatureId that was passed to the script as a parameter            
 $f = $Web.Features | ?{$_.DefinitionId -like $FeatureId}            
    #If the feature was found, $f won't be null            
 if($f -ne $null)            
 {            
        #If the feature was found, check the SPFeature.Definition.Status property, to see if the feature is activated (online)            
  if($f.Definition.Status -eq "Online")            
  {            
   Write-Host "Disabling feature,"$f.Definition.DisplayName -f Green            
            #If the feature is online, then record the features details in a new custom object (this will get returned at the end of the function)            
   $fm = $FeatureInfo | Select-Object *;            
   $fm.Id = $f.Definition.Id;            
   $fm.DisplayName = $f.Definition.DisplayName;            
   $fm.WebUrl = $Web.Url;            
   $fm.Status = $f.Definition.Status;            
   $fm.TimeActivated = $f.TimeActivated            
   #Add the custom object to the $matches array (a list that will contain a custom object for each instance of a feature found)            
   $matches += $fm;            
    #Check the -ReportOnly switch. If the switch wasn't passed to the function, it will be false.             
   if($ReportOnly -eq $false)            
   {            
                #Disable the feature            
    Disable-SPFeature -Identity $f.Definition.Id -Url $Web.Url -Confirm:$false
   }               
  }            
 }            
  #Check the -Recurse switch. If the switch wasn't passed to the function, it will be false. Also check if the web has any sub-webs. If both of these checks are true, then check the sub-webs            
 if($Recurse -eq $true -and $Web.Webs.Count -gt 0)            
 {            
  Write-Host "Checking sub webs" -f Blue            
  foreach($sw in $Web.Webs)            
  {   #Here we want to call the same function again, against the sub-webs. The $matches variable is incremented with the results returned from the new call to the function.             
   if($ReportOnly -eq $false)            
   {            
    $matches += Outst-SPFeature -Web $sw -FeatureId $FeatureId -Recurse            
   }            
   else            
   {            
    $matches += Outst-SPFeature -Web $sw -FeatureId $FeatureId -Recurse -ReportOnly            
   }                 
  }              
 }            
 #The $matches array (containing the list that contains custom objects for each instance of a feature found) is returned.            
 return $matches;            
}            

And this is how it can be called:

#Run this command to disable the feature on all webs in all webapplications.            
$disabledfeature = Get-SPWebApplication | Get-SPSite -Limit All | Get-SPweb -Limit All |Foreach {Outst-SPFeature $_ -FeatureId "a5557f3e-102c-401e-9eae-1e7fcf4340d0" -Recurse}

And this is how you export the results to an xml file that can reloaded at a later time:

#Run this command to disable the feature on all webs in all webapplications, and save a report of all the feature instances that were disabled to C:\Temp\DisabledFeatureReport.xml
Get-SPWebApplication | Get-SPSite -Limit All | Get-SPweb -Limit All |Foreach {Outst-SPFeature $_ -FeatureId "a5557f3e-102c-401e-9eae-1e7fcf4340d0" -Recurse} | Export-Clixml -Path C:\Temp\DisabledFeatureReport.xml

Wednesday, 4 June 2014

Renaming a HNSC (Host Named Site Collection) in a SharePoint Farm with Multiple Web Applications

Recently I've been involved in migrating a large SharePoint farm from SharePoint 2010 to SharePoint 2013. The farm has multiple web applications, and HSNC's have been created in different web applications. Part of the migration has required us to rename a number of HNSC's (Host Named Site Collections) for the development, testing and staging environments.

I'll start by saying, Microsoft recommends using one web application (SPWebApplication) per farm if you are planning on using HNSC (host named site collections). But that doesn't stop people using HSNC's in a farm with multiple SharePoint web applications.

So the short of this is, if you have a SharePoint farm with multiple web applications, and you want to rename a HNSC within one of those web applications, you have to add a new URL (using New-SPSiteUrl) to an "unused" zone, and then manually add the IIS bindings into SharePoint Web Applications IIS site, for each server in the farm that accepts requests for the site collection. To see some PowerShell on how to do this, skip to the bottom of the article!

If you google or bing "rename SPSIte", you'll get a number of articles and posts about renaming both "path based" and "host named" site collections. There are a few PowerShell ways for achieving this, including Rename-SPSite, and SPSite.Rename(newurl).

In our case, none of these approaches worked. Renaming "path based" site collections works fine, and renaming "host named" site collections also works fine... as long as you have configured your farm in the supported way for HNSC's (with one web application). The environment we were upgrading had many web applications, and HNSC's in more than one of those web applications.

So after a bit of investigation, we (and by we, I mean a colleague of mine, Elaine Van Bergen) pointed out that IIS has to know which IIS site it should send the request to(there is always a single IIS site behind each SharePoint web application). Each IIS site has bindings (for domain names) that tell IIS which urls the site will serve requests for.

After checking the IIS bindings on the IIS site behind one of the SharePoint web applications where we had added a new URL for a HNSC, I found there was no binding there for the new URL (domain) I'd added to the HNSC.

If that doesn't already make sense, hopefully the following example explains it clearer.

For the example, we have an IIS site listening on port 80, with a binding for blackburn.trainstation.com. This IIS site will respond to any requests sent to the address blackburn.trainstation.com/* (e.g. http://blackburn.trainstation.com/default.aspx). We also have another IIS site with a binding for hawthorn.trainstation.com. Both of these IIS Sites have SharePoint web applications behind them, and each web application has a single SharePoint site collection. The site collections are host named site collections.

In this scenario, we have a SharePoint web application for the blackburn.trainstation.com, with an IIS site behind it listening on port 80, with the binding blackburn.trainstation.com. Web requests for blackburn.trainstation.com/* will be accepted by this IIS site, which will pass it onto SharePoint to look up the site, and respond to the request.



If a new URL, "gleniris.trainstation.com", is added to the "intranet" zone of the blackburn.trainstation.com web application, what would happen? When the request is received by the web server, IIS looks for an "IIS" site that has a binding for gleniris.trainistation.com. In our example there are no sites with this binding, but there is a site with a binding for "*" (anything), so IIS will send the request to that "IIS" site (Default Website). Because the Default IIS site doesn't actually have any sort of site behind it, an HTTP 404 is returned.



Once a binding for gleniris.trainstation.com is added to the blackburn.trainstation.com IIS site, requests for gleniris.trainstation.com will now be responded to by the IIS site with the binding. If this is an IIS site with a SharePoint web application behind it, the request is then handed off to SharePoint to fulfil.



So, getting back to the original problem, how do we rename a HNSC on a SharePoint farm with multiple web applications (without doing a backup-spsite, delete-spsite, restore-spsite)? It's not ideal. The way we found was to use Add-SPSiteURL cmdlet to add the new URL to a new zone (one other than the default zone). After doing this, we needed to manually add an IIS binding for the SharePoint web application's IIS website, on all the SharePoint WFE's (web servers), to tell the IIS site to respond to requests for the new URL.

Building on our example, the commands for this looks like this:

#Add a new URL for the HNSC http://blackburn.trainstation.com            
$site = Get-SPSite http://blackburn.trainstation.com            
Add-SPSiteUrl $site -URL http://gleniris.trainstation.com -Zone Intranet            
            
#Add the IIS binding, for gleniris.trainstation.com on port 80 (remember, this will need to be performed on each WFE)            
Import-Module "WebAdministration"            
New-WebBinding -Name $site.WebApplication.Name -Protocol http -Port 80 -IPAddress "*" -HostHeader gleniris.trainstation.com

It's not pretty, but it is a workaround if you find yourself in this position.

As I said at the start, this is probably one reason why Microsoft recommends using HNSC with one web application. If you are using one web application, you can configure it with a binding of "*". Doing this ensures the IIS site responds to all requests, and hands them off to SharePoint to satisfy. SharePoint contains the list of HNSC's and takes care of responding to valid requests.

Friday, 23 May 2014

Using PowerShell, CSOM, and some Hokey Pokey to copy files between two different SharePoint farms

Recently I needed to migrate a large number of images from a SharePoint 2010 farm, to a new SharePoint 2013 farm. I had local server access to the SharePoint 2013 farm servers, but only browser based access to the SharePoint 2010 farm.

PowerShell remoting wasn't an option, so I couple together a couple methods to come up with a solution. Use CSOM (via PoweShell) to get a list of the files (images) on the remote SharePoint library. Then use the WebClient class to download the file, and finally, the standard server based object model to add the downloaded image to the destination SharePoint farm libary.

What the hell, I hear you say?! But why not just use CSOM all the way?! Because, well, actually, I have no good reason. Probably because I was in a hurry, and this was the fastest way I could achieve my objective in the short amount of time I had! And sometimes, a quick solution to a problem is more valuable than "technically perfect" approach! They're my excuses anyway.

So, here's the PowerShell code I used to do it. It's commented, and if nothing else, it demonstrates that there is usually more than one way to approach a problem!! Oh yeah, it also demonstrates using PowerShell and SharePoint CSOM together!


function Import-FilesFromRemoteLibraryUsingCSOM{            
    [CmdletBinding()]                        
    Param(                           
            [parameter(Mandatory=$true)][string]$SourceWebApplicationUrl,            
            [parameter(Mandatory=$true)][string]$SourceWebUrl,            
            [parameter(Mandatory=$true)][string]$SourceLibrary,            
      [parameter(Mandatory=$true)][string]$DestinationWebUrl,            
            [parameter(Mandatory=$true)][string]$DestinationLibrary            
        )            
    #Load the Microsoft SharePoint Client Dll's            
    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";             
    #Using the CSOM in PowerShell is very similar to using it in C#, with just a few minor differences.            
    #Get the Client Context, using the SourceWeb URL passed to the function. This is the web we will be getting the files from.             
    $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($SourceWebUrl)            
    #Load the web            
    $w = $ctx.Web            
    $ctx.Load($w)            
    #Get and load the list that contains the files we want to migrate            
    $l = $w.Lists.GetByTitle($SourceLibrary)            
    $ctx.Load($l)            
    #Create a new query. This is where we could filter the list items (files) that we're going to migrate. In this example, we're just limiting the results to 1000 items.            
    $query = New-Object Microsoft.SharePoint.Client.CamlQuery            
    $query.ViewXml = "1000"            
    $items = $l.GetItems($query)            
    #Load the items            
    $ctx.Load($items)            
    #Execute the loaded queries.             
    $ctx.ExecuteQuery()            
    if($l -eq $null){Write-Host "List not found." -f Red;return;}            
    Write-Host "Attempting to import all library files." -f darkyellow;            
    #Create an array for holding a list of file URL's            
    $fileUrls = @();            
    #Get the destination web and list. Remember that this code is running on the destination SharePoint server, so we can just use the standard SharePoint cmdlet's            
    $dw = Get-SPWeb $DestinationWebUrl;            
    $dl = $dw.Lists[$DestinationLibrary];            
    #Create a WebClient object. We'll use this to download each file to a byte array, which we can upload into SharePoint.             
    #I chose to use this method, as it was quick, I was short on time, and I was having trouble getting a byte array using CSOM.            
    #It demonstrates an alternate approach to a problem!             
    $wc = New-Object System.Net.WebClient;             
    $credentials = [System.Net.CredentialCache]::DefaultCredentials;            
    $wc.Credentials = $credentials;            
    #For each item in the list of items retrieved from our source server (using CSOM), we create the URL, download the file as a byte array, and then upload it to the destination SharePoint server            
    foreach($i in $items){            
        #Load the item and file            
        $item = $l.GetItemById($i.Id);            
        $file = $item.File;            
        $ctx.Load($item);            
        $ctx.Load($file);            
        #Execute the query to get the items we loaded            
        $ctx.ExecuteQuery();            
        #Create the Url for the file            
        $itemUrl = ([String]::Format("{0}{1}",$SourceWebApplicationUrl,$item["FileRef"]));              
        $fileName = $itemUrl.Substring($itemUrl.LastIndexOf("/")+1);            
        Write-Host "Downloading file $itemUrl";            
        #Download the file as a byte array            
        $file = $wc.DownloadData($itemUrl);            
        #Upload the file to the destination SharePoint library            
        $nf = $dw.Files.Add(([String]::Format("{0}/{1}/{2}",$dw.Url,$dl.RootFolder.Url,$fileName)),$file,$true);            
        #If the destination list requires files to be checked in, then check it in!            
        if($nf.RequiresCheckout)            
        {               
         $nf.CheckIn("Checked in during migration","MajorCheckIn")             
        }              
        Write-host "Added file $fileName" -f Green;            
    }                
}

Tuesday, 6 May 2014

Use PowerShell to add and remove items in the Quick Launch menu on a SharePoint 2013 site

Here's a quick snippet of PowerShell that demonstrates adding menu items to the Quick Launch menu in a SharePoint 2013 site.

The example is based on a newly added Business Intelligence site, though the code will work on any site. The goal is to remove the existing quick launch menu items, and replace them with a list of report document libraries, grouped under a heading called, "Reports".

The original site looks like this:



The following PowerShell adds a list of links (to the report lists) onto the quick launch menu. Note that the new menu items are defined in the $listsToAddToNav array.

#SharePoint site url            
$weburl = "http://devhv131/sites/bi"            
#An array of SharePoint lists that you want to add to the navigation menu            
$listsToAddToNav = @('Building Management','Capital Programmes','Employee Services','Financial','General Reports','Payroll','Property','Purchasing');            
            
#Get the SPWeb object for the site url            
$w = get-spweb $weburl            
#Get the quick launch menu            
$ql = $w.Navigation.QuickLaunch;            
#Create the root node that all the lists will be displayed under.            
$n = New-Object Microsoft.SharePoint.Navigation.SPNavigationNode("Reports","",$true);            
#Add the new node to the quick launch menu            
$n = $ql.AddAsFirst($n);             
#Add all the lists as new child nodes, to the "Reports" node            
foreach($list in $listsToAddToNav)            
{            
    #Create a new child node, for the current folder. The URL of the folder is constructed using the name of the folder, and the web URL            
 $cn = New-Object Microsoft.SharePoint.Navigation.SPNavigationNode($list,([String]::Format("{0}/{1}",$w.Url,$list.Replace(" ","%20"))),$true);             
    #Add the childnode to the parent node, which in this case, is the "Reports" navigation node.             
    #Add the child as the "last node". As the nodes being added are in alphabetical order, this will preserve the node order, as A-Z            
    $n.Children.AddAsLast($cn);            
    #Update (save) the navigation            
 $w.Update()            
}

After running the PowerShell above, the new menu items have been added.



This next bit of PowerShell removes the unwanted menu items from the quick launch. Note that the unwanted menu items are defined in the $nodesToDelete array.

#Create an array of nodes that you want to delete. The array contains the node names.        
$nodesToDelete = @('Dashboards','Data Connections','PerformancePoint Content','Recent','Libraries')            
#For each node, delete it from the Quick Launch menu            
foreach($dnname in $nodesToDelete)            
{            
 #Get the node            
 $dn = $w.Navigation.QuickLaunch | where { $_.Title -eq $dnname }            
 if($dn -eq $null){continue;}            
 Write-Host "Deleting navigation node, $dnname"            
    #If the node wasn't null, delete it!             
 $ql.Delete($dn);            
    #Update (save) the navigation            
 $w.Update();            
}

Finally, with the unwanted navigation menu items removed, the site now looks the way we want it!



Tuesday, 29 April 2014

PowerShell Runas

Have you ever had a PowerShell script where you need to run a command with administrative privileges (under the security context of another user), without being prompted to enter credential information?

Recently I needed to run a SharePoint script under two different security context's. Most of the script needed to run as a farm administrator, while a small section of the script needed to run under the security context of the farm service account.

Ingredients:

Invoke-Command
1 x new PSSession Object
1 x PSCredential Object
1 x set of credentials (a username and password)

You can run a script block using the Invoke-Command cmdlet. The Invoke-Command cmdlet can take a PSSession object as a parameter. Using a PSSession object, you can create a PowerShell session with a different security context (as long as you have the username and password of the different user account) by passing the alternate credentials as a PSCredential object.

It's all very easy, and here's a few examples examples.

Basic PowerShell code to run a script block using alternate credentials:

#Variables to store the username and password for the alternate account            
$SPFarmAccountName = "sp-dev-farm";            
$FarmAccountPassword = "NotMyPswd";            

#Convert the plain text password to a SecureString, required to create the PSCredential object
$FarmAccountPasswordAsSecureString = $FarmAccountPassword | ConvertTo-SecureString -Force -AsPlainText            

#Create the PSCredential object using the alternate users username and password            
#Note that the Domain name has been prepended to the username, using the $env:userdomain variable, which represents the current domain.            
$credential = New-Object System.Management.Automation.PsCredential("$env:userdomain\$SPFarmAccountName",$FarmAccountPasswordAsSecureString)              

#Create a new PowerShell session in the security context of the alternate user, using the PSCredential object we just created            
$farmSvcAccSession = New-PSSession -Credential $credential;            

#Write some text to the PowerShell Window, that prints the username from the current context 
Write-Host "This PowerShell command is running under the current users context, $env:userdomain\$env:username" -f magenta            

#Pass the PSSession object to Invoke-Command, and write some text to the PowerShell Window, that prints the username from the current context of the PSSession object (which will be the security context of the alternate user)            
Invoke-Command -Session $farmSvcAccSession -Script { Write-Host "Hello, this script block is running under the security context of the SharePoint Farm Account, $env:userdomain\$env:username" -f Green; }            

#Write some more text to the PowerShell Window, that shoes the security context has returned to the original user            
Write-Host "And now we return to the current users context, $env:userdomain\$env:username" -f magenta



Run a PowerShell script block using alternate credentials, passing a parameter to the elevated session (in this case, a URL)

$waUrl = "http://portal.dev.local";            
$SPFarmAccountName = "sp-dev-farm";            
$FarmAccountPassword = "NotMyPswd";            
$FarmAccountPasswordAsSecureString = $FarmAccountPassword | ConvertTo-SecureString -Force -AsPlainText            
$credential = New-Object System.Management.Automation.PsCredential("$env:userdomain\$SPFarmAccountName",$FarmAccountPasswordAsSecureString)              
$farmSvcAccSession = New-PSSession -Credential $credential;            
Write-Host "This PowerShell command is running under the current users context, $env:userdomain\$env:username" -f magenta            
Invoke-Command -Session $farmSvcAccSession -Script { Write-Host "Hello, this script block is running under the security context of the SharePoint Farm Account, $env:userdomain\$env:username The web URL is"$args[0] -f Green; } -Args $waUrl            
Write-Host "And now we return to the current users context, $env:userdomain\$env:username" -f magenta



Run a PowerShell script block using alternate credentials, passing a URL as a parameter to the elevated session, and then running some SharePoint commands.

$waUrl = "http://portal.dev.local";            
$SPFarmAccountName = "sp-dev-farm";            
$FarmAccountPassword = "NotMyPswd";            
$FarmAccountPasswordAsSecureString = $FarmAccountPassword | ConvertTo-SecureString -Force -AsPlainText            
$credential = New-Object System.Management.Automation.PsCredential("$env:userdomain\$SPFarmAccountName",$FarmAccountPasswordAsSecureString)              
$farmSvcAccSession = New-PSSession -Credential $credential;            
Write-Host "This PowerShell command is running under the current users context, $env:userdomain\$env:username" -f magenta            
Invoke-Command -Session $farmSvcAccSession -Script { Add-PSSnapin "Microsoft.SharePoint.PowerShell";  Write-Host "Hello, this script block is running under the security context of the SharePoint Farm Account, $env:userdomain\$env:username The web URL is"$args[0] -f Green; $eweb = Get-SPWeb $args[0];$eweb.SiteAdministrators | ForEach-Object {Write-Host "Removing user $_ from site administrators group." -f green;}; Write-Host "Just joking! Do you think we're crazy?!" -f yellow;  } -Args $waUrl            
Write-Host "And now we return to the current users context, $env:userdomain\$env:username" -f magenta


See Also:



Monday, 24 March 2014

Testing SharePoint Email Alerts in UAT and DEV Environments

Introduction

This post describes installing an SMTP server (SMTP4Dev) that can be used to capture all email sent from a SharePoint farm, without delivering the email to the recipients mailbox.

This is handy to have setup in a Development or UAT (user acceptance testing) environment, where you want to test or analyse email alerts sent from SharePoint, without actually having the emails delivered to the end user.

The two scenarios presented here

1. Installing and configuring SMTP4Dev on a SharePoint server (or other server) that doesn’t already have a process listening on port 25 (e.g. the IIS SMTP service)
2. Installing and configuring SMTP4Dev on a SharePoint server that already has a service listening on port 25 (e.g. the IIS SMTP service)

Introduction to SMTP4Dev

SMTP4Dev is a console application used to receive email via SMTP. Email received by SMTP4Dev can be inspected or deleted. However, SMTP4Dev does not deliver email to a destination mailbox. It can listen on any port (the default port is 25), and will accept email while it’s running. It can be configure for anonymous or authenticated connections.

SMTP4Dev is great for being able to capture and analyse emails sent in a UAT or Development environment, without needing an email infrastructure (like Exchange and Outlook).

Downloaded SMTP4Dev here: http://smtp4dev.codeplex.com/ 

Installing SMTP4Dev

Installing and running SMTP4Dev is very simple. Follow these steps:
  1. Download SMTP4Dev from codeplex (http://smtp4dev.codeplex.com/) and save the zip file to the local file system. E.g. C:\Tools\smtp4dev.zip
  2. Extract the zip files contents to the same location
  3. Double click the smtp4dev.exe
  4. Done!

When SMTP4Dev is opened, it will start listening on Port 25 by default.



If there is already another process using port 25 (e.g. the IIS SMTP service), SMTP4Dev will show an error message about a socket address (the configure port is already in use).



Configure SharePoint Outbound email with SMTP4Dev running on Port 25

If no other process is running on port 25, SMTP4Dev will use the default SMTP port (25) to listen on. Once you start SMTP4Dev, by default it will begin to listen  for email on port 25, using anonymous authentication.

To finish the configuration, set the Outbound Email server for the farm to the FQDN (Fully Qualified Domain Name) of the SharePoint application server running SMTP4Dev.

  1. Open the Central Admin site
  2. Click on System Settings
  3. Click on Configure outgoing e-mail settings
  4. Enter the FQDN for the SharePoint application server that is running SMTP4Dev, into the Outbound SMTP Server textbox.
  5. Set the From address and Reply-to address
  6. Click OK to save the settings.

Configure SharePoint Outbound email with SMTP4Dev running on a Custom Port

If there is already another process listening port 25, you will need to configure SMTP4Dev to listen on another port. Configuring SMTP4Dev to listen on another port is easy. However, SharePoint will only send outbound email to an SMTP server listening on Port 25. So additional configuration is required to get this working.

Configuring SMTP4Dev to listen on a custom port might happen in a scenario where all of the SharePoint servers in a UAT or Development farm already have the IIS SMTP service installed (for inbound email).
Consider the following scenario:

  • The SharePoint application server that SMTP4Dev is installed on has the host name SP13App01.mydomain.com
  • The SharePoint Application server, SP13App01.mydomain.com, already has the IIS SMTP service configured (on Port 25) for Inbound Email (functionality that allows people to email a SharePoint folder).
  • The IIS SMTP service is configured to receive email for the SharePoint farms domain alias (e.g. production-sharepoint.mydomain.com)
  • The Outbound email SMTP server for the farm is set to sp13app01.mydomain.com
  • The IIS SMTP server on sp13app01 is configured to relay email for all remote domains to the SMTP smart host going.nowhere.local, over port 19876.
  • There is a dns entry in the hosts file on sp13app01 for going.nowhere.local that uses the localhost IP address 127.0.0.1
  • SMTP4Dev is configured to listen on Port 19876



In this scenario, any email sent to an address in the production-sharepoint.mydomain.com will be picked up by one of the SharePoint servers running the IIS SMTP service (on Port 25) and saved into the IIS mail Drop folder (for SharePoint to process). If an email sent to the SMTP server isn’t addressed to the production-sharepoint.mydomain.com domain, the IIS SMTP server will;

  • (WFE servers): discard the email
  • (Application server): forward (relay) the message to the SMTP smart host.

An SMTP smart host is an SMTP server that will accept email for any domain from a source SMTP server (the sending SMTP server), and forward that email to a destination SMTP server responsible for the emails domain.

In practice, the IIS SMTP on the SharePoint WFE servers should never receive email addressed to a foreign domain. However, the IIS SMTP service on the SharePoint application server will receive email addressed to the production-sharepoint.domain.com domain, as well as emails sent from the SharePoint farm itself. This is because the SharePoint server sp13app.domain.com has been set as the outgoing email server for the farm.

In this scenario, SMTP4Dev is configured as the Smarthost. It will receive all emails sent to the SharePoint Application server’s IIS SMTP service (running on Port 25) that are destined for a foreign domain (e.g. a list alert configured to send new list item alerts to a domain user).
To configure SharePoint for this type of scenario, follow these steps;
  1. Configure SMTP4Dev to listen on a custom port
  2. Add a host alias to the hosts file of the server running SMTP4Dev
  3. Configure the IIS SMTP service on the SharePoint application server to relay all email destined for a foreign domain to the host alias configured in step 2
  4. Configure the SharePoint application server as the outbound SMTP server for the farm.

1. Configure SMTP4Dev to listen on a custom port

  1. Open SMTP4Dev
  2. Click Options
  3. From the Options dialog, click the Server tab
  4. Change the Port Number to a custom port value between 1025 and 65000. In the example, we use port 19876



    Note: Whatever port you choose, make sure no other process is listening on that port. To check this, you can use the NETSTAT command, and pipe the results to the FIND command. For example, so check if a process is listening on port 19786, use the following command at a command prompt.

    Netstat –a | Find “:19876”

2. Add a host alias to the host file

Each SharePoint server in the farm is configured to send outbound email to a single SharePoint application server. The IIS SMTP service on the SharePoint application server is configured to forward (relay) all e-mail for foreign domains to a Smart Host. The “Smart Host” is actually an SMTP service (SMTP4Dev) running on itself, listening on a different port.

When you configure the Smart Host in the IIS SMTP services, the UI prevents you from adding the hostname of the current server as the Smart Host.

To work around this limitation, add an alias to the hosts file on the SharePoint application server. The alias can be anything (though it shouldn’t be a hostname used anywhere else), but the IP address must be set to the local server (127.0.0.1).

In this example, the alias is set to going.nowhere.local


3. Configure IIS SMTP service to relay email to a Smart Host

Configure the IIS SMTP service (on the SharePoint application server) to forward email (for remote domains) to itself.
  1. Open the IIS Admin 6.0 console
  2. Expand the local server
  3. Expand the SMTP Virtual Server
  4. Right click the virtual server, and click Properties
  5. Click on the Access tab
  6. Configure the Relay settings
    Note: Be careful configuring relay settings. Ensure that you restrict the list of servers (IP Addresses) allowed to use this server as an open (unauthenticated) SMTP relay.


  7. From the example we’ve been using, we are going to allow the two SharePoint WFE servers to relay through this SMTP server un-authenticated.
  8. After configuring the SMTP Relay settings, you need to configure the Smart Host that will be used to forward all foreign email to. Click on the Delivery tab.
  9. Click Outbound connections
  10. In the Outbound Connections dialog, enter the TCP port that SMTP4Dev is listening on. In the example, this is port 19876
  11. Click Ok to save the changes.
  12. From the Delivery tab, click Advanced.
  13. In the Advanced Delivery dialog, set the Smart Host. This will be the alias name you added to the hosts file. In the example, we used going.nowhere.local
  14. Click Ok to save the changes.
  15. This completes the configuration of the IIS STMP service.

In Summary

  1. The IIS SMTP Service on all SharePoint servers is configured to accept emails sent to SharePoint web applications. In the examples, the SharePoint servers accept email sent to the production-sharepoint.mydomain.local domain. E.g. an email addressed to myshareddocumentlibrary@production-sharepoint.mydomain.local
  2. The IIS SMTP Service on the SharePoint application server is configured to forward all foreign email (that is, email sent to other domains) to an SMTP Smart Host (hosted on the same server), called going.nowhere.local, over port 19876.
  3. The SMTP service listening on port 19876 is SMTP4Dev.  SMTP4Dev will receive all email sent to it, so that it can be viewed. E-mail will never reach the mailbox of the intended recipient, which is the behaviour we want.

4. Configure the Outbound email settings for the farm

The final step is to configure the farm to send all outbound email to the SharePoint application server. After doing this, the IIS SMTP service on the SharePoint application server will receive all outbound email, and forward it on to the testing SMTP service, SMTP4Dev (listening on port 19876).

References