Freitag, 21. Oktober 2016

Auflistung aller e-Mail fähigen Listen in der SharePoint-Farm

Es sollen "mal schnell" alle Listen & Bibliotheken aufgelistet werden, für die ein e-Mail Empfang konfiguriert ist? Nichts leichter als das:


<#
  .SYNOPSIS
   Lists all E-Mail enabled lists
   If all is fine here you'll need to check
    Resolve-DnsName <your-mail-domain-here> -Type MX #make sure the "right" server is the MX
 Get-WindowsFeature smtp-server #make sure InstallState is "installed"
 Get-Service SMTPSVC #make sure Status is "running"

  .EXAMPLE
   Get-SPEmailEnabledLists
   Gets all e-Mail enabled lists from all webs in all site collections from all WebApplications. This may be a lot to check: You have been warned.

  .EXAMPLE
   Get-SPEmailEnabledLists -WebApplication (Get-SPWebApplication http://sp.dev/)
    Gets all e-Mail enabled lists from all webs in all site collections of the given WebApplication

  .EXAMPLE
   Get-SPSite | ?{$_.Url - match "my."} | Get-SPEmailEnabledLists
    Gets all e-Mail enabled lists from all webs in the given site collections

  .EXAMPLE
   Get-SPWeb http://sp.dev/sites/simple/sub | Get-SPEmailEnabledLists
    Gets all e-Mail enabled lists from the given web
#>
[CmdletBinding()]
param(
    [Parameter(
        HelpMessage="restrict search to this web",
        ValueFromPipeline=$true)]
    [Microsoft.SharePoint.SPWeb[]]$Web,
    [Parameter(
        HelpMessage="restrict search to this site collection",
        ValueFromPipeline=$true)]
    [Microsoft.SharePoint.SPSite[]]$Site,
    [Parameter(
        HelpMessage="restrict search to this webApplication",
        ValueFromPipeline=$true)]
    [Microsoft.SharePoint.Administration.SPWebApplication[]]$WebApplication
)

Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction Inquire | Out-Null

# check incoming mail settings
$serverAddr = "<unconfigured>";
$settings = (Get-SPFarm).Services | ?{ $_.TypeName -match "incoming e-mail" } | select -First 1
if((-not $settings) -or (-not $settings.Enabled)){
    $url = "_admin/IncomingEmail.aspx";
    $ca = (Get-SPWebApplication -IncludeCentralAdministration) | ?{ $_.IsAdministrationWebApplication } | select -First 1
    if($ca) {
        $url = $ca.Url + $url;
    } else {
        $url = "http://<your central admin>/$url";
    }
    Write-Warning "No incoming email-settings found, or incoming email is deactivated. check $url !"
} else {
    $serverAddr = $settings.ServerDisplayAddress;
}

# collect the lists to check
if(-not $Web) {
    Write-Verbose "no web given - getting all webs from site";
    if(-not $Site) {
        Write-Verbose "no site given - getting all sites from webApplication";
        if(-not $WebApplication) {
            Write-Verbose "no webApplication given - getting all webApplications";
            $WebApplication = Get-SPWebApplication;
        }

        $Site = $WebApplication | % { $_.Sites };
    }

    $Web = $Site | %{ $_.AllWebs };
}


# check the lists
$lists = $Web | %{ $_.Lists }
Write-Verbose "checking $($lists.Count) lists...";
$lists | %{
    if(($_.CanReceiveEmail) -and ($_.EmailAlias)) {
        [PSCustomObject]@{
            Title = $_.Title;
            Url = "$($_.ParentWeb.Url)/$($_.RootFolder.Url)";
            EmailAlias = "$($_.EmailAlias)@$serverAddr";
        }
    }
}


Das Skript kann als Eingabe für Out-GridView oder auch Export-Csv verwendet werden - oder auch nur in der Console betrachtet...
Und so ganz nebenbei prüft das Skript noch ob der e-Mail Empfang in der Farm überhaupt eingerichtet ist..

Dienstag, 17. Mai 2016

SharePoint eine Test-Mail versenden lassen

Auf die spannende Frage "Warum versendet SharePoint keine Mails?" gibt es in der Regel nur wenige Antworten.
Wenn die Einstellungen der ausgehenden Mails ("Configure outgoing e-mail settings") in SharePoint gut aussehen hilft meistens nur testen und das ULS-Log beobachten.

Das folgende PowerShell-Skript kann verwendet werden um schnell Test-Mails aus dem SharePoint versenden zu lassen (Dabei kann man dann das ULS-Log - am besten mittels ULS Viewer - beobachten)
function Send-SPTestMail {
  <#
      .SYNOPSIS
        Sends a Test-Mail using SharePoint-Standard Tools
        returns "TRUE", if the mail was successfully "taken in" with SharePoint (most probably also given to smtp...)
        and "FALSE" if there was any failure to do so.
        Check your ULS-Logs for further investigation if the result is FALSE, or your smtp-server-logs if the result is TRUE
  
      .PARAMETER To
        "TO" field of the mail.

      .PARAMETER Web
        The Url to the SPWeb to access via the SharePoint e-Mail utilities

      .PARAMETER From
        "FROM" field of the mail. defaults to "someone@example.com"
      
      .PARAMETER Subject
        "SUBJECT" field of the mail. Defaults to "Test" and a Date.

      .PARAMETER Body
        "BODY" of the mail. Defaults to some nice text.
  
      .EXAMPLE
        Send-SPTestMail -Web http://path.to/Web -From me@acme.com
        Sends a simple mail using all the nice deafults..
   #>
   [CmdletBinding()]
   param (
      [Parameter(Mandatory=$true)]
      [string]$Web,
      [Parameter(Mandatory=$true)]
      [string]$To,
      [Parameter(Mandatory=$false)]
      [string]$From = "someone@example.com",
      [Parameter(Mandatory=$false)]
      [string]$Subject = $null,
      [Parameter(Mandatory=$false)]
      [string]$Body = $null
   )

    Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction Stop
    $headers = new-object System.collections.Specialized.StringDictionary
    if(!$Subject) {
        $Subject = "Test @ $(Get-Date)";
    }

    $headers.add("to",$To)
    $headers.add("from",$From)
    $headers.add("Subject",$Subject)

    if(!$Body){
        $Body = "Auto-Generated body of test-mail <ul><li>Generated at $(Get-Date)</li><li>Headers:<ul>";
        $headers | % { $Body += "<li>$($_.Name): $($_.Value)</li>";  }
        $Body += "</ul></li><li>Sent via SharePoint-Web at $($Web)</li></ul>";
    }

    $spweb = $null;
    try {
        $spweb = Get-SPWeb $Web -ErrorAction Stop
        [Microsoft.SharePoint.Utilities.SPUtility]::SendEmail($spweb,$headers,$Body)
    } finally {
        if($spweb -ne $null) {
            $spweb.Dispose();
        }
    }
 }

Donnerstag, 28. April 2016

web.config-transform - "my way"

Ich mag die Web.Config-Transforms. Sehr sogar.
Was mich daran stört ist, dass ich eine Build-Konfiguration für jede Stage/jeden Server benötige. Um das zu umgehen (und ggf. vielleicht auch einmal andere xml-Dateien zu transformieren) kann die Transformation auch manuell über einen MSBuild-Task starten.
Im Regelfall habe ich meine Transformationen im "Configs\"-Verzeichnis mit der Benennung web.[stage].conf - die lasse ich dann automatisiert beim build & publish erstellen. Dafür habe ich das folgende targets-file, das ich einfach im csproj einbinde:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <SourceWebConfig>$(ProjectDir)\Web.config</SourceWebConfig>
        <OutputPath Condition="'$(OutputPath)' == ''">$(ProjectDir)\bin</OutputPath>
        <TempWebConfig>$(OutputPath)\Web.Temp.config</TempWebConfig>
        <TransformationsBaseDir>$(ProjectDir)\Configs</TransformationsBaseDir>
        <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
        <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
    </PropertyGroup>
    <Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets"/>

    <ItemGroup>
        <WebConfigTransforms Include="$(TransformationsBaseDir)/*.config">
            <TransformDest>$(OutputPath)\%(WebConfigTransforms.Filename).config</TransformDest>
        </WebConfigTransforms>
    </ItemGroup>

    <Target Name="TransformAllWebConfigs">
        <!-- Does the real transformation of all web-config transforms... -->
        <Copy SourceFiles="$(SourceWebConfig)"
              DestinationFiles="$(TempWebConfig)" />
        <Message Text="transforming %(WebConfigTransforms.Identity) to %(WebConfigTransforms.TransformDest)"
                 Importance="high"/>
        <TransformXml Source="$(TempWebConfig)"
                      Transform="%(WebConfigTransforms.Identity)"
                      Destination="%(WebConfigTransforms.TransformDest)"
                      StackTrace="true" />
        <Delete Files="$(TempWebConfig)" />
    </Target>

    <Target Name="TransformAfterBuild"
            AfterTargets="Build">
        <!-- after build, build all web.configs, too -->
        <CallTarget Targets="TransformAllWebConfigs" />
        <Message Text="TransformAllWebConfigs has run after build!"
                 Importance="high"/>
    </Target>
    <Target Name="MimicVsTransforms"
            AfterTargets="PreTransformWebConfig">
        <!-- this should mimic the original VS-behaviour -->
        <CallTarget Targets="TransformAllWebConfigs" />
    </Target>
    <Target Name="CopyTransformedForPublish"
            AfterTargets="CopyAllFilesToSingleFolderForPackage">
        <!-- "GatherAllFilesToPublish" as AfterTarges seems to work, too. But only when Publish is called from within VS. -->
        <Copy SourceFiles="@(WebConfigTransforms->'%(TransformDest)')"
              DestinationFolder="$(WPPAllFilesInSingleFolder)" />
    </Target>
</Project>

Die Einbindung im projekt-file erfolgt ganz "normal".
    <Import Project="$(ProjectDir)\..\TransformWebConfig.targets" Condition="'$(TransformationsBaseDir)' == ''" />

In einem aktuellen Projekt bestand die Anforderung die Transformationen in Unterverzeichnissen abzulegen (da es noch mehr spezifische Dateien pro Stage gab...). Dafür habe einfach die ItemGruop wie folgt angepasst:
<ItemGroup>
 <WebConfigTransforms Include="$(TransformationsBaseDir)\**\web.config">
  <StageName>$([System.String]::new('%(RecursiveDir)').TrimEnd('\\'))</StageName>
  <TransformDest>$(OutputPath)\web.%(StageName).config</TransformDest>
 </WebConfigTransforms>
</ItemGroup>

Mittwoch, 9. März 2016

Erstellen einer StringCollection in PowerShell, OutputType und unrolling

Im SharePoint-Kontext benötigt man immer mal wieder eine StringCollection (System.Collections.Specialized.StringCollection). Um diese zu erstellen wollte ich "kurz" eine PowerShell-Funktion dazu erstellen. Drei Punkte habe ich dabei gelernt:
  1. Der OutputType einer Funktion wird nur für Autovervollständigung z.B. in der ISE benutzt. Der "wirkliche" Rückgabe-Typ entspricht immer der eigentlichen Rückgabe zur Laufzeit.
  2. Rückgaben, die eine Auflistung sind werden "unrolled". D.h. diese erscheinen in der Rückgabe dann als normales Array.
  3. "unrolling" kann in der Rückgabe verhindert werden wenn dieser ein Komma vorgestellt wird. (Also "return ,$foo;" - reichlich komisch...)
Der Code im Ergebnis sieht dann so aus:
function Create-StringCollection {
   <#
      .SYNOPSIS
        Creates a StringCollection from a array of strings

      .PARAMETER txt
        strings to convert

      .EXAMPLE
        @("foo", "bar") | Create-StringCollection
   #>
   [CmdletBinding()]
   [OutputType([System.Collections.Specialized.StringCollection])]
   param (
      [Parameter(Mandatory=$true, ValueFromPipeline=$True)]
      [string[]]$txt
   )
   Begin {
      $coll = New-Object System.Collections.Specialized.StringCollection;
   }
   Process {
      $coll.Add($txt) | Out-Null;
   }
   End {
      ,$coll; # hack to prevent powershell from "unrolling" the enumerable
   }
}

Montag, 31. August 2015

SharePoint liefert debug-JS

Das im SharePoint verwendete JavaScript ist minified, aber manchmal möchte ich es trotzdem debuggen...

Das geht über eine Einstellung in der web.config oder über das ScriptMode-Attribut am ScriptManager in der MasterPage.

Ein Attribut in der web.config scheint mir einfacher als die MasterPage anzupassen... Mit der Einschränkung, dass eine manuelle Anpassung der web.config im SharePoint keine wirklich gute Idee ist. Microsoft hat uns dafür die SPWebConfigModification mitgegeben.

Damit das ein- und abschalten der debugging-JS trotzdem einfach ist habe ich mir das folgende Skript gebaut:

<#
    .SYNOPSIS
    Set your SharePoint webApplication to use debugging JS-files 
    instead of minified.

    .DESCRIPTION
    SharePoint uses minified JavaScript (e.g. SP.js) but is also able to
    use debugging versions of the files instead. (e.g. SP.debug.js)
    Setting SharePoint to deliver debug-JS requires a setting
    in web.config. 
    This script sets the required value by adding a SpWebConfigModifaction
    to your WebApplication.

    .PARAMETER WebApplication
    URL to WebApplication

 .PARAMETER Remove
    Remove this modification (and thereby restore the original web.config)

    .EXAMPLE
    .\Enable-SpJsDebugging.ps1 -WebApplication http:/your.farm/site/
    Apply webConfig-Modification to allow for debugging-js to be used.

    .EXAMPLE
    .\Enable-SpJsDebugging.ps1 -WebApplication http:/your.farm/site/ -Remove
    Remove webConfig-Modification and retore original web.config
#>
[CmdletBinding()]
param (
 [Parameter(Mandatory=$true)]
    [string]$WebApplication,

    [Parameter(Mandatory=$false)]
    [switch]$Remove = $false
)

if ( (Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null )
{
 Add-PSSnapin Microsoft.SharePoint.PowerShell
}

$name = "PowerShell.Scripts.EnableSpJsDebugging";

function Get-Modifications([Microsoft.SharePoint.Administration.SPWebApplication]$webApp){
    return $webApp.WebConfigModifications | ?{ $_.Owner -eq $name }
}

function Remove-Modifications([Microsoft.SharePoint.Administration.SPWebApplication]$webApp, 
    [Microsoft.SharePoint.Administration.SPWebService]$contentService){
    $mods = @();
    $removed = @()
    Get-Modifications($webApp) | %{ $mods = $mods + $_};
    $mods | %{ $removed += $webApp.WebConfigModifications.Remove($_) };
    $webApp.Update();
    $contentService.ApplyWebConfigModifications();
    $success = $removed | ?{ $_ -eq $true };
    $fails = $removed | ?{ $_ -eq $false };
    
    Write-Output "Removed $($success.Length) modifications.";
    if($fails.length > 0) {
        Write-Output "$($fails.Length) tries failed.";
    }
}

function Add-Modification([Microsoft.SharePoint.Administration.SPWebApplication]$webApp, 
    [Microsoft.SharePoint.Administration.SPWebService]$contentService){
    $mod = New-Object "Microsoft.SharePoint.Administration.SPWebConfigModification";
    $mod.Path = "configuration/system.web/compilation";
    $mod.Name = "debug";
    $mod.Sequence = 0;
    $mod.Owner = $name;
    $mod.Type = [Microsoft.SharePoint.Administration.SPWebConfigModification+SPWebConfigModificationType]::EnsureAttribute;
                     
    $mod.Value = "true";

    $unused = $webApp.WebConfigModifications.Add($mod);
    $webApp.Update();
    $contentService.ApplyWebConfigModifications();
    Write-Output "Added modification.";
}

$gc = Start-SPAssignment
try {
    $webApp = Get-SPWebApplication $WebApplication -AssignmentCollection $gc;
    $contentService = [Microsoft.SharePoint.Administration.SPWebService]::ContentService

    if($Remove){
        Remove-Modifications $webApp $contentService;
        Exit;
    }

    if((Get-Modifications $webApp).length -gt 0) {
        Write-Output "Modification already applied...";
        Exit;
    }

    Add-Modification $webApp $contentService;
} finally {
    Stop-SPAssignment $gc
}

Die Verwendung ist dann denkbar einfach:
> .\Enable-SpJsDebugging.ps1 -WebApplication http:/your.farm/site/
> .\Enable-SpJsDebugging.ps1 -WebApplication http:/your.farm/site/ -Remove

Sicherheitswarnung im Visual Studio beim debuggen des IIS

Wenn man im Visual Studio den Debugger an den IIS hängt kommt vorher ein Warnhinweis.

Das ist jetzt keine neue Information - und nach einer kurzen Suche kommt auch schnell wieder auf irgend einen Punkt an dem steht welcher Registry-Key auf welchen Wert gesetzt werden muss...

Da es mich aber immer etwas stört vorher suchen zu müssen und mich anschließend durch die Registry zu hangeln habe ich das mal als PowerShell-Skript abgelegt:

<#
 .SYNOPSIS
 Disable warning-messages when attaching VS to iis/w3wp.

 .DESCRIPTION
 When you attach VS to iis/w3wp for debugging, VS displays a warning.
 This script disables theese warnings by modifying the registry accordingly.
 WARNING: Make sure VS is not running when you start this script!

 .PARAMETER VsVersion
 The VS-version to modify - e.g. 10.0 for VS2010 or 14.0 for VS2015

 .PARAMETER AllVersions
 modify all installed Versions of VS

 .PARAMETER ReEnable
 Undo the changes done by this script - i.e. enable the warnings

 .PARAMETER IgnoreRunningVS
 continue script execution, even if a running VS is detected.

 .EXAMPLE
 .\Disable-VsDebuggerWarning.ps1 -VsVersion 12.0
 Disable the warning only for VS2013

#>

[CmdletBinding(DefaultParameterSetName="all")]
param (
 [Parameter(Mandatory=$false, ParameterSetName="all")]
    [switch]$AllVersions = $false,

 [Parameter(Mandatory=$true, ParameterSetName="one")]
    [string]$VsVersion,

 [Parameter(Mandatory=$false, ParameterSetName="all")]
    [Parameter(Mandatory=$false, ParameterSetName="one")]
    [switch]$ReEnable = $false,

 [Parameter(Mandatory=$false, ParameterSetName="all")]
    [Parameter(Mandatory=$false, ParameterSetName="one")]
    [switch]$IgnoreRunningVS = $false
)

$vsPath = "HKCU:\Software\Microsoft\VisualStudio\";

function Get-AllVsVersions()
{
    if(!(Test-Path $vsPath ))
    {
        Write-Error "No VisualStudio installations found. Exiting";
        Exit;
    }

    $versions = Get-ChildItem $vsPath | Where-Object { !$_.Name.EndsWith("_Config") } | Split-Path -Leaf

    Write-Host "$($versions.Length) VS installations found in registry: $versions"

    return $versions;
}

function Disable-Warning($version)
{
    $key = "DisableAttachSecurityWarning";
    $basePath = Join-Path $vsPath ($version + "\Debugger");
    $path = Join-Path $basePath $key;
    Write-Output "Setting $path to DWORD: $dword";

    # it's possible the key does not exist (e.g. in VS2015)
    $nonExistingItem = ((Get-ItemProperty $basePath -Name $key -ErrorAction SilentlyContinue) -eq $null);
    if($nonExistingItem)
    {
        if(!(Test-Path $basePath))
        {
            Write-Error "$basePath does not exist! Unable to proceed! Exiting";
            return;
        }

        New-ItemProperty $basePath -Name $key -Value $dword -PropertyType "DWord" 
    }
    else
    {
        Set-ItemProperty $basePath -Name $key -Value $dword
    }
}

$isVsRunning = ((Get-Process | where { $_.Name -eq "devenv" }).length -gt 0)

if($isVsRunning)
{
    Write-Host "A running VisualStudio was detected..." -ForegroundColor Red;
    if(!$IgnoreRunningVS)
    {
        Write-Output "If you want to prceed anyway, use the ""-IgnoreRunningVS""-Switch. Exiting.";
        Exit;
    }

    Write-Host "Ignoring the running VisualStudio... WARNING: this might not acually work! You have been warned!" -ForegroundColor Yellow;
}

$dword = 1;
if($ReEnable)
{
    $dword = 0;
}

$versions = @();
if($AllVersions)
{
    $versions = Get-AllVsVersions
}
elseif(![string]::IsNullOrWhiteSpace($VsVersion))
{
    $versions += $VsVersion
}
else
{
    Write-Host "Neither -VsVersion nor -AllVersions given. Nothing to do. :-(" -ForegroundColor Yellow
}

$versions | %{ Disable-Warning $_ }

Das Skript kann für eine bestimmte Version (z.B. mit "-VsVersion 10.0") oder einfach für alle Versionen ("-AllVersions") aufgerufen werden.

Donnerstag, 5. Februar 2015

Finden von nicht verwendeten CSS Klassen

Die Fragestellung des Tages: Wie finde ich CSS-Klassen, die im HTML definiert wurden aber nie in Styles verwendet werden?
Die Antwort scheint "fast unmöglich" zu sein. Leider.
Ich habe mal versucht eine Näherung zu finden - der folgende code (angewendet auf eine beliebige Seite in der jQuery geladen ist) listet die im code definierten CSS-Klassen und gleicht diese anschießend mit den in den Styles definierten Selektoren ab:

(function($){
 var classesInUse = {
   _definedProperties: []
  },
  selectorsInUse = {
   _definedProperties: []
  };

 //
 // find all used classes
 //
 $('[class]').each(function(){
  var $this = $(this),
   className = this.className,
   classes = className.split(/\s+/);
  $.each(classes, function(_,c) {
   if(c) {
    //console.log('found a usage of class ' + c);
    if(!classesInUse[c]) {
     classesInUse[c] = {
      elems: [$this]
     };
     classesInUse._definedProperties.push(c);
    } else {
     classesInUse[c].elems.push($this);
    }
   }
  });
 });
 classesInUse._definedProperties.sort();

 $.each(classesInUse._definedProperties, function(_, p){
  console.log('class ' + p + ' was used ' + classesInUse[p].elems.length + ' times in HTML.');
 });
 
 //
 // find all defined css selectors
 // 
 $.each(document.styleSheets, function(_, style) {
  $.each(style.cssRules, function(_, rule) {
   var selector = rule.selectorText;
   if(selector) {
    //console.log('found a definition of css-selector ' + selector);
    if(!selectorsInUse[selector]) {
     selectorsInUse[selector] = {
      count: 1
     };
     selectorsInUse._definedProperties.push(selector);
    } else {
     selectorsInUse[selector].count += 1;
    }
   }
  });
 });
 selectorsInUse._definedProperties.sort();

 $.each(selectorsInUse._definedProperties, function(_, p){
  console.log('selector "' + p + '" was defined ' + selectorsInUse[p].count + ' times in CSS.');
 });
 
 
 //
 // compare classes & selectors
 //
 $.each(classesInUse._definedProperties, function(_, klass){
  var classSelector = '.' + klass,
   usages = 0;
  $.each(selectorsInUse._definedProperties, function(_, cssSelector){
   if(cssSelector.indexOf(classSelector) > -1) {
    usages += 1;
   }
  });
  classesInUse[klass].usagesInCss = usages;
  if(usages === 0) {
   console.log('class ' + klass + ' was not used in CSS!');
  } else {
   console.log('class ' + klass + ' was used in CSS ' + usages + ' times.');
  }
 });
})(jQuery);

Der Code hat natürlich noch diverse Nachteile:

  • Nur das aktuell geladene HTML im DOM wird nach Klassen durchsucht.
  • Nur das aktuell aktive CSS wird nach Selektoren durchsucht.
  • Klassen, die verwendet werden um Elemente einfach im HTML per jQuery auffindbar zu machen werden ggf. als "nicht verwendet" aufgelistet.
Die Ergebnisse sind also mit entsprechender Vorsicht zu verwenden!


Der umgekehrte Fall (Selektoren zu finden, die nicht verwendet werden) scheint übrigens gängiger zu sein: Dafür gibt es tools.