MailDaemon.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\MailDaemon.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName MailDaemon.Import.DoDotSource -Fallback $false if ($MailDaemon_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName MailDaemon.Import.IndividualFiles -Fallback $false if ($MailDaemon_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) if ($doDotSource) { . (Resolve-Path $Path) } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText((Resolve-Path $Path)))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'MailDaemon' -Language 'en-US' function Copy-Module { <# .SYNOPSIS Copies a module from one computer to another. .DESCRIPTION Copies a module from one computer to another. All transfers done via WinRM / Powershell Remoting. .PARAMETER ModuleName The name of the module to copy. Also accepts a path to the module root folder. .PARAMETER ModuleObject A specific module instance to copy (returned by Get-Module). .PARAMETER FromComputer The computer from which to pick up the module. Localhost by default. Accepts and reuses PSSession objects. .PARAMETER ToComputer The computer(s) on which to install the module. Accepts and reuses PSSession objects. .PARAMETER Credential The credentials to use when connecting to computers. .EXAMPLE PS C:\> Copy-Module -ModuleName BeerFactory -ToComputer server1 Copies the module 'BeerFactory' from localhost to server1 #> [CmdletBinding(DefaultParameterSetName = 'Object')] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'String', Position = 0)] [string[]] $ModuleName, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Object')] [PSModuleInfo[]] $ModuleObject, [Parameter(Mandatory = $true)] [PSFComputer[]] $ToComputer, [PSFComputer] $FromComputer = $env:COMPUTERNAME, [PSCredential] $Credential ) begin { $receiveScript = { param ( [string] $Module ) #region Specified a path $uri = [uri]$Module if ($uri.IsFile) { if (-not (Test-Path $Module)) { return [pscustomobject]@{ Module = $Module Success = $false Data = @() } } $sourcePath = "$($Module)\*" $moduleName = (Get-Module $Module -ListAvailable).Name $moduleVersion = (Get-Module $Module -ListAvailable).Version } #endregion Specified a path #region Specified a module name else { $moduleObject = Get-Module $Module | Sort-Object Version -Descending | Select-Object -First 1 if (-not $moduleObject) { return [pscustomobject]@{ Module = $Module Success = $false Data = @() } } $sourcePath = "$($moduleObject.ModuleBase)\*" $moduleName = $moduleObject.Name $moduleVersion = $moduleObject.Version } #endregion Specified a module name #region Gather module object $tempPath = "$($env:TEMP)\$(New-Guid).zip" $workingFolder = New-Item -Path $env:TEMP -Name (New-Guid) -ItemType Directory # Copy item is important, as the zip commands cannot access locked dlls, copy-item can. Copy-Item -Path $sourcePath -Destination $workingFolder.FullName -Recurse Compress-Archive -Path "$($workingFolder.FullName)\*" -DestinationPath $tempPath [pscustomobject]@{ Name = $moduleName Version = $moduleVersion Data = [System.IO.File]::ReadAllBytes($tempPath) Module = $Module Success = $true } Remove-Item $tempPath Remove-Item $workingFolder.FullName -Recurse -Force #endregion Gather module object } $installScript = { param ( $Modules ) #region Update the modules foreach ($module in $Modules) { $installRoot = "$($env:ProgramFiles)\WindowsPowerShell\Modules" if (-not (Test-Path "$($installRoot)\$($module.Name)")) { $null = New-Item -Path $installRoot -Name $module.Name -ItemType Directory -Force } $root = New-Item -Path "$($installRoot)\$($module.Name)" -Name $module.Version -ItemType Directory -Force $tempPath = "$($env:TEMP)\$(New-Guid).zip" [System.IO.File]::WriteAllBytes($tempPath, $Module.Data) Expand-Archive -Path $tempPath -DestinationPath $root.FullName -Force Remove-Item $tempPath } #endregion Update the modules } } process { foreach ($name in $ModuleName) { Write-PSFMessage -String 'Copy-Module.ReceivingModule' -StringValues $FromComputer, $name $module = Invoke-PSFCommand -ComputerName $FromComputer -Credential $Credential -ScriptBlock $receiveScript -ArgumentList $name if (-not $module.Success) { Stop-PSFFunction -String 'Copy-Module.ReceivingModule.Failed' -StringValues $FromComputer, $name -Continue -SilentlyContinue -Cmdlet $PSCmdlet } Write-PSFMessage -String 'Copy-Module.InstallingModule' -StringValues $name, ($ToComputer -join ", ") Invoke-PSFCommand -ComputerName $ToComputer -Credential $Credential -ScriptBlock $installScript -ArgumentList $module } foreach ($object in $ModuleObject) { Write-PSFMessage -String 'Copy-Module.ReceivingModule' -StringValues $FromComputer, $object.ModuleBase $module = Invoke-PSFCommand -ComputerName $FromComputer -Credential $Credential -ScriptBlock $receiveScript -ArgumentList $object.ModuleBase if (-not $module.Success) { Stop-PSFFunction -String 'Copy-Module.ReceivingModule.Failed' -StringValues $FromComputer, $object.ModuleBase -Continue -SilentlyContinue -Cmdlet $PSCmdlet } Write-PSFMessage -String 'Copy-Module.InstallingModule' -StringValues $object.ModuleBase, ($ToComputer -join ", ") Invoke-PSFCommand -ComputerName $ToComputer -Credential $Credential -ScriptBlock $installScript -ArgumentList $module } } } function Test-Module { <# .SYNOPSIS Tests for module existence. .DESCRIPTION Tests whether a module - or set of modules - exists on the target machine(s). Includes support for version requirements (minimum or maximum). .PARAMETER Name Name of the module(s) to search for. .PARAMETER Version The version constraint. Whether that is the minimum, maximum or exactly this version is governed by the -Test parameter. The same version constraint will be applied to all modules specified! For custom versions per module, please use the -Module parameter to specify a hashtable with the mapping. .PARAMETER Module The combination of modules and versions to test. Specify the modulename as key and the version as value. E.g.: @{ MailDaemon = '1.0.0' } Specify '0.0' in order to not test about any specific version. .PARAMETER Test How to test for version. By default, the test will search for 'GreaterEqual' (that is: At least the specified version). Supported scenarios: 'LesserThan', 'LesserEqual', 'Equal', 'GreaterEqual', 'GreaterThan' Note on Lesser* comparisons: This only tests whether a version below the limit is present. It does not Test that NO greater version is available! .PARAMETER Quiet Disables output objects and instead returns $true if all modules specified meet the requirements, $false if not so. .PARAMETER ComputerName The computers on which to test. Uses WinRM / PowerShell Remoting to perform test. .PARAMETER Credential The credentials to use for connecting to computers for the test. Will be ignored for localhost. .EXAMPLE PS C:\> Test-Module -Name 'MyModule' Tests whether the module MyModule is available in any version. .EXAMPLE PS C:\> Test-Module -Name MailDaemon -Version 1.1.0 -ComputerName 'server1', 'Server2' Tests whether the module MailDaemon is available in at least version 1.1.0 on the computers server1 and server2. .EXAMPLE PS C:\> Test-Module -Name PSFramework -Version 1.0.0 -Quiet -Test 'Equal' Returns $true if the module PSFramework exists locally in exactly version 1.0.0, $false otherwise. .EXAMPLE PS C:\> Test-Module -Module @{ PSFramework = '1.0.0'; MailDaemon = '1.1.0' } -Test 'LesserThan' Returns whether PSFramework is present in any version less than 1.0.0 Returns whether MailDaemon is present in any version less than 1.1.0 #> [CmdletBinding(DefaultParameterSetName = 'Name')] param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Name')] [string[]] $Name, [Parameter(Position = 1, ParameterSetName = 'Name')] [version] $Version = '0.0.0.0', [Parameter(Mandatory = $true, ParameterSetName = 'Hash')] [hashtable] $Module = @{ }, [ValidateSet('LesserThan', 'LesserEqual', 'Equal', 'GreaterEqual', 'GreaterThan')] [string] $Test = 'GreaterEqual', [switch] $Quiet, [Parameter(ValueFromPipeline = $true)] [PSFComputer[]] $ComputerName = $env:COMPUTERNAME, [AllowNull()] [PSCredential] $Credential ) begin { #region Prepare Module parameter $moduleHash = $Module foreach ($moduleName in $Name) { $moduleHash[$moduleName] = $Version } foreach ($key in ([string[]]$moduleHash.Keys)) { $moduleHash[$key] = $moduleHash[$key] -as [Version] if (-not $moduleHash[$key]) { $moduleHash[$key] = ([Version]'0.0.0.0') } } #endregion Prepare Module parameter #region Validation Scriptblock $scriptBlock = { param ( [hashtable] $ModuleHash, [string] $Test, [bool] $Quiet ) #region Utility Functions function Write-Result { [CmdletBinding()] param ( [string] $Name, $Success, [AllowNull()] [AllowEmptyCollection()] $VersionsFound, [string] $Test ) $result = [bool]$Success [PSCustomObject]@{ Name = $Name Success = $result VersionsFound = $VersionsFound ComputerName = $env:COMPUTERNAME Test = $Test } } #endregion Utility Functions #region Validate each module specified foreach ($module in $ModuleHash.Keys) { $modulesFound = Get-Module -Name $module -ListAvailable if ($Quiet -and (-not $modulesFound)) { return $false } if ($ModuleHash[$module] -le '0.0.0.0') { Write-Result -Name $module -Success $modulesFound -VersionsFound $modulesFound.Version -Test $Test continue } #region Quiet Validation [Calls Continue] if ($Quiet) { switch ($Test) { 'LesserThan' { if (-not ($modulesFound | Where-Object Version -LT $ModuleHash[$module])) { return $false } } 'LesserEqual' { if (-not ($modulesFound | Where-Object Version -LE $ModuleHash[$module])) { return $false } } 'Equal' { if (-not ($modulesFound | Where-Object Version -EQ $ModuleHash[$module])) { return $false } } 'GreaterEqual' { if (-not ($modulesFound | Where-Object Version -GE $ModuleHash[$module])) { return $false } } 'GreaterThan' { if (-not ($modulesFound | Where-Object Version -GT $ModuleHash[$module])) { return $false } } } continue } #endregion Quiet Validation [Calls Continue] switch ($Test) { 'LesserThan' { Write-Result -Name $module -Success ($modulesFound | Where-Object Version -LT $ModuleHash[$module]) -VersionsFound $modulesFound.Version -Test $Test } 'LesserEqual' { Write-Result -Name $module -Success ($modulesFound | Where-Object Version -LE $ModuleHash[$module]) -VersionsFound $modulesFound.Version -Test $Test } 'Equal' { Write-Result -Name $module -Success ($modulesFound | Where-Object Version -EQ $ModuleHash[$module]) -VersionsFound $modulesFound.Version -Test $Test } 'GreaterEqual' { Write-Result -Name $module -Success ($modulesFound | Where-Object Version -GE $ModuleHash[$module]) -VersionsFound $modulesFound.Version -Test $Test } 'GreaterThan' { Write-Result -Name $module -Success ($modulesFound | Where-Object Version -GT $ModuleHash[$module]) -VersionsFound $modulesFound.Version -Test $Test } } } #endregion Validate each module specified if ($Quiet) { return $true } } #endregion Validation Scriptblock } process { Invoke-PSFCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock $scriptBlock -ArgumentList $moduleHash, $Test, $Quiet.ToBool() -HideComputerName } } function Add-MDMailContent { <# .SYNOPSIS Adds content to a pending email. .DESCRIPTION Adds content to a pending email. Use this command to incrementally add to the mail sent. .PARAMETER Body Add text to the mail body. .PARAMETER Attachments Add files to the list of files to send. .EXAMPLE PS C:\> Add-MDMailContent -Body "Phase 3: Completed" Adds the line "Phase 3: Completed" to the email body. #> [CmdletBinding()] Param ( [string] $Body, [string[]] $Attachments ) begin { if (-not $script:mail) { $script:mail = @{ } } } process { if ($Body) { if (-not ($script:mail["Body"])) { $script:mail["body"] = $Body } else { $script:mail["Body"] = $script:mail["Body"], $Body -join "`n" } } } } function Install-MDDaemon { <# .SYNOPSIS Configures a computer for using the Mail Daemon .DESCRIPTION Configures a computer for using the Mail Daemon. This can include: - Installing the scheduled task - Creating folder and permission structure - Setting up the mail daemon configuration This action can be performed both locally or against remote computers .PARAMETER ComputerName The computer(s) to work against. Defaults to localhost, but can be used to install the module and set up the task across a wide range of computers. .PARAMETER Credential The credentials to use when connecting to computers. .PARAMETER NoTask Create the scheduled task. .PARAMETER TaskUser The credentials of the user the scheduled task will be executed as. .PARAMETER PickupPath The folder in which emails are queued for delivery. .PARAMETER SentPath The folder in which emails that were successfully sent are stored for a specified time before being deleted. .PARAMETER DaemonUser The user to grant permissions needed to function as the Daemon account. This grants read/write access to all working folders. .PARAMETER WriteUser The user/group to grant permissions to needed to queue email. This grants write-only access to the mail inbox. .PARAMETER MailSentRetention The time to keep successfully sent emails around. .PARAMETER SmtpServer The mailserver to use for sending emails. .PARAMETER SenderDefault The default email address to use as sender. This is used for mails queued by a task that did not specify a sender. .PARAMETER SenderCredential The credentials to use to send emails. Will be stored in an encrypted file that can only be opened by the taskuser and from the computer it is installed on. .PARAMETER RecipientDefault Default email address to send the email to, if the individual script queuing the email does not specify one. .EXAMPLE PS C:\> Install-MDDaemon -ComputerName DC1, DC2, DC3 -TaskUser $cred -DaemonUser "DOMAIN\MailDaemon" -SmtpServer 'mail.domain.org' -SenderDefault 'daemon@domain.org' -RecipientDefault 'helpdesk-t2@domain.org' Configures the mail daemon NoTask on the servers DC1, DC2 and DC3 #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [PSFComputer[]] $ComputerName = $env:COMPUTERNAME, [PSCredential] $Credential, [switch] $NoTask, [PSCredential] $TaskUser, [string] $PickupPath, [string] $SentPath, [string] $DaemonUser, [string[]] $WriteUser, [Timespan] $MailSentRetention, [string] $SmtpServer, [string] $SenderDefault, [PSCredential] $SenderCredential, [string] $RecipientDefault ) begin { #region Repetitions (ugly) # Specifying repetitions directly in the commandline is ugly. # It ignores explicit settings and requires copying the repetition object from another task. # Since we do not want to rely on another task being available, instead I chose to store an object in its XML form. # By deserializing this back into an object at runtime we can carry an object in scriptcode. $object = @' <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"> <Obj RefId="0"> <TN RefId="0"> <T>Microsoft.Management.Infrastructure.CimInstance#Root/Microsoft/Windows/TaskScheduler/MSFT_TaskRepetitionPattern</T> <T>Microsoft.Management.Infrastructure.CimInstance#MSFT_TaskRepetitionPattern</T> <T>Microsoft.Management.Infrastructure.CimInstance</T> <T>System.Object</T> </TN> <ToString>MSFT_TaskRepetitionPattern</ToString> <Props> <S N="Duration">P1D</S> <S N="Interval">PT30M</S> <B N="StopAtDurationEnd">false</B> <Nil N="PSComputerName" /> </Props> <MS> <Obj N="__ClassMetadata" RefId="1"> <TN RefId="1"> <T>System.Collections.ArrayList</T> <T>System.Object</T> </TN> <LST> <Obj RefId="2"> <MS> <S N="ClassName">MSFT_TaskRepetitionPattern</S> <S N="Namespace">Root/Microsoft/Windows/TaskScheduler</S> <S N="ServerName">C0020127</S> <I32 N="Hash">-1401671928</I32> <S N="MiXml"><CLASS NAME="MSFT_TaskRepetitionPattern"><PROPERTY NAME="Duration" TYPE="string"></PROPERTY><PROPERTY NAME="Interval" TYPE="string"></PROPERTY><PROPERTY NAME="StopAtDurationEnd" TYPE="boolean"></PROPERTY></CLASS></S> </MS> </Obj> </LST> </Obj> </MS> </Obj> </Objs> '@ $repetitionObject = [System.Management.Automation.PSSerializer]::Deserialize($object) #endregion Repetitions (ugly) #region Setup Task Configuration if (-not $NoTask) { $action = New-ScheduledTaskAction -Execute powershell.exe -Argument "-NoProfile -Command Invoke-MDDaemon" $triggers = @() $triggers += New-ScheduledTaskTrigger -AtStartup -RandomDelay "00:15:00" $triggers += New-ScheduledTaskTrigger -At "00:00:00" -Daily if ($TaskUser) { $principal = New-ScheduledTaskPrincipal -UserId $TaskUser.UserName -LogonType Interactive } else { $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType Interactive } $taskItem = New-ScheduledTask -Action $action -Principal $principal -Trigger $triggers -Description "Mail Daemon task, checks for emails to send at a specified interval. Uses the internal MailDaemon module." $taskItem.Author = Get-PSFConfigValue -FullName 'MailDaemon.Task.Author' -Fallback "$($env:USERDOMAIN) IT Department" $taskItem.Triggers[1].Repetition = $repetitionObject $parametersRegister = @{ TaskName = 'MailDaemon' InputObject = $taskItem } if ($TaskUser) { $parametersRegister["User"] = $TaskUser.UserName $parametersRegister["Password"] = $TaskUser.GetNetworkCredential().Password } } #endregion Setup Task Configuration #region Preparing Parameters $parameters = @{ } foreach ($key in $PSBoundParameters.Keys) { if ($key -notin 'PickupPath', 'SentPath', 'MailSentRetention', 'SmtpServer', 'SenderDefault', 'RecipientDefault') { continue } $parameters[$key] = $PSBoundParameters[$key] } $paramMainInstallCall = @{ ArgumentList = $parameters Credential = $Credential } #endregion Preparing Parameters #region The Main Setup Scriptblock $paramMainInstallCall["ScriptBlock"] = { param ( $Parameters ) Import-Module -Name PSFramework Import-Module -Name MailDaemon Set-MDDaemon @parameters #region Set file permissions if (-not (Test-Path $_Config.MailPickupPath)) { $null = New-Item $_Config.MailPickupPath -Force -ItemType Directory } if (-not (Test-Path $_Config.MailSentPath)) { $null = New-Item $_Config.MailSentPath -Force -ItemType Directory } if ($Parameters.DaemonUser) { Update-MDFolderPermission -DaemonUser $Parameters.DaemonUser } if ($Parameters.WriteUser) { Update-MDFolderPermission -WriteUser $Parameters.WriteUser } #endregion Set file permissions } #endregion The Main Setup Scriptblock } process { #region Ensure Modules are installed $testResults = Test-Module -ComputerName $ComputerName -Credential $Credential -Module @{ MailDaemon = $script:ModuleVersion PSFramework = (Get-Module -Name PSFramework).Version } $failedTests = $testResults | Where-Object Success -EQ $false if ($failedTests) { $grouped = $failedTests | Group-Object Name foreach ($groupSet in $grouped) { Copy-Module -ModuleName (Get-Module $groupSet.Name).ModuleBase -ToComputer $groupSet.Group.ComputerName } } #endregion Ensure Modules are installed $paramMainInstallCall['ComputerName'] = $ComputerName Invoke-PSFCommand @paramMainInstallCall #region Securely store credentials if ($PSBoundParameters.ContainsKey('SenderCredential')) { $parametersSave = @{ ComputerName = $ComputerName Credential = $SenderCredential Path = 'C:\ProgramData\PowerShell\MailDaemon\senderCredentials.clixml' } if ($TaskUser) { $parametersSave['AccessAccount'] = $TaskUser } Save-MDCredential @parametersSave $parametersInvoke = @{ $parametersInvoke['ComputerName'] = $ComputerName } Invoke-PSFCommand @parametersInvoke -ScriptBlock { Set-MDDaemon -SenderCredentialPath "C:\ProgramData\PowerShell\MailDaemon\senderCredentials.clixml" } } #endregion Securely store credentials #region Setup Task if (-not $NoTask) { foreach ($computerObject in $ComputerName) { if ($ComputerName.Type -like 'CimSession') { $parametersRegister["CimSession"] = $computerObject.InputObject } elseif (-not $ComputerName.IsLocalhost) { $parametersRegister["CimSession"] = $ComputerName } $null = Register-ScheduledTask @parametersRegister } } #endregion Setup Task } } function Invoke-MDDaemon { <# .SYNOPSIS Processes the email queue and sends emails .DESCRIPTION Processes the email queue and sends emails. Should be scheduled using a scheduled task. Recommended Setting: - Launch on boot with delay - Launch on Midnight - Repeat every 30 minutes for one day .EXAMPLE PS C:\> Invoke-MDDaemon Processes the email queue and sends emails #> [CmdletBinding()] Param ( ) begin { if (-not (Test-Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath'))) { $null = New-Item -Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath') -ItemType Directory } if (-not (Test-Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailSentPath'))) { $null = New-Item -Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailSentPath') -ItemType Directory } } process { #region Send mails foreach ($item in (Get-ChildItem -Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath'))) { $email = Import-Clixml -Path $item.FullName # Skip emails that should not yet be processed if ($email.NotBefore -gt (Get-Date)) { continue } # Build email parameters $parameters = @{ SmtpServer = Get-PSFConfigValue -FullName 'MailDaemon.Daemon.SmtpServer' Encoding = ([System.Text.Encoding]::UTF8) ErrorAction = 'Stop' } if ($email.To) { $parameters["To"] = $email.To } else { $parameters["To"] = Get-PSFConfigValue -FullName 'MailDaemon.Daemon.RecipientDefault' } if ($email.From) { $parameters["From"] = $email.From } else { $parameters["From"] = Get-PSFConfigValue -FullName 'MailDaemon.Daemon.SenderDefault' } if ($email.Cc) { $parameters["Cc"] = $email.Cc } if ($email.Subject) { $parameters["Subject"] = $email.Subject } else { $parameters["Subject"] = "<no subject>" } if ($email.Body) { $parameters["Body"] = $email.Body } if ($null -ne $email.BodyAsHtml) { $parameters["BodyAsHtml"] = $email.BodyAsHtml } if ($email.Attachments) { $parameters["Attachments"] = $email.Attachments } if ($script:_Config.SenderCredentialPath) { $parameters["Credential"] = Import-Clixml (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.SenderCredentialPath') } Write-PSFMessage -Level Verbose -String 'Invoke-MDDaemon.SendMail.Start' -StringValues @($email.Taskname, $parameters['Subject'], $parameters['From'], ($parameters['To'] -join ",")) -Target $email.Taskname try { Send-MailMessage @parameters } catch { Stop-PSFFunction -String 'Invoke-MDDaemon.SendMail.Failed' -StringValues $email.Taskname -ErrorRecord $_ -Continue -Target $email.Taskname } Write-PSFMessage -Level Verbose -String 'Invoke-MDDaemon.SendMail.Success' -StringValues $email.Taskname -Target $email.Taskname # Remove attachments only if ordered and maail was sent successfully if ($email.Attachments -and $email.RemoveAttachments) { foreach ($attachment in $email.Attachments) { Remove-Item $attachment -Force } } # Update the timestamp (the timeout for deletion uses this) and move it to the sent items folder $item.LastWriteTime = Get-Date try { Move-Item -Path $item.FullName -Destination (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailSentPath') -Force -ErrorAction Stop } catch { Write-PSFMessage -Level Warning -String 'Invoke-MDDaemon.ManageSuccessJob.Failed' -StringValues $email.Taskname -Target $email.Taskname } } #endregion Send mails } end { #region Cleanup expired mails foreach ($item in (Get-ChildItem -Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailSentPath'))) { if ($item.LastWriteTime -lt (Get-Date).Add((-1 * (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailSentRetention')))) { Remove-Item $item.FullName } } #endregion Cleanup expired mails } } function Save-MDCredential { <# .SYNOPSIS Stores credentials securely for use by the specified account. .DESCRIPTION This command encrypts credentials to a protected credentials file in the file system. This is designed to allow storing credential objects for use by scheduled task that run as SYSTEM or a service account. .PARAMETER TargetCredential The credentials to encrypt and write to file. .PARAMETER Path The path where to store the credential. Always considered as local path from the computer it is registered on. .PARAMETER AccessAccount The account that should have access to the credential. Defaults to the system account. Offer a full credentials object for a regular user account. .PARAMETER ComputerName The computer(s) to write the credential to. .PARAMETER Credential The credentials to use to authenticate to the target system. NOT the credentials stored for reuse. .EXAMPLE PS C:\> Save-MDCredential -ComputerName DC1,DC2,DC3 -TargetCredential $cred -Path "C:\ProgramData\PowerShell\Tasks\tesk1_credential.clixml" Saves the credentials stored in $cred on the computers DC1, DC2, DC3 for use by the SYSTEM account #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [PSCredential] $TargetCredential, [Parameter(Mandatory = $true)] [string] $Path, [PSCredential] $AccessAccount, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PSFComputer[]] $ComputerName = $env:COMPUTERNAME, [PSCredential] $Credential ) process { $parameters = @{ ArgumentList = $TargetCredential, $Path, $AccessAccount ComputerName = $ComputerName Credential = $Credential } Invoke-PSFCommand @parameters -ScriptBlock { Param ( [PSCredential] $Credential, [string] $Path, [PSCredential] $AccessAccount ) #region Folder Management if (Test-Path -Path $Path) { $item = Get-Item $Path if ($item.PSIsContainer) { $folder = $item.FullName $file = Join-Path $folder 'Credential.clixml' } else { $folder = Split-Path $item.FullName $file = $item.FullName } } else { if ([System.IO.Path]::GetExtension($Path)) { $folder = Split-Path $Path $file = $Path } else { $folder = $Path $file = Join-Path $folder 'Credential.clixml' } } if (-not (Test-Path -Path $folder)) { $null = New-Item -Path $folder -ItemType Directory -Force -ErrorAction Stop } #endregion Folder Management #region Access Privileges $accessUserName = $AccessAccount.UserName if (-not $accessUserName) { $accessUserName = "SYSTEM" } $acl = Get-Acl -Path $folder if (-not ($acl.Access | Where-Object IdentityReference -like $accessUserName | Where-Object { ($_.FileSystemRights -Band 278) -and ($_.FileSystemRights -Band 65536) })) { $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($accessUserName, 'Read, Write', 'Allow') $null = $acl.AddAccessRule($rule) $acl | Set-Acl -Path $folder } #endregion Access Privileges #region Create Task $folderCleaned = (Get-Item $folder).FullName $credFile = "{0}\{1}.txt" -f $folderCleaned, ([guid]::NewGuid()) $task = { $password = [System.IO.File]::ReadAllText("<credfile>") Remove-Item -Path "<credfile>" $credential = New-Object PSCredential("<username>", ($password | ConvertTo-SecureString -AsPlainText -Force)) $credential | Export-Clixml -Path "<exportPath>" } $commandString = $task.ToString().Replace("<credfile>", $credFile).Replace("<username>", $Credential.UserName).Replace("<exportPath>", $file) $encodedCommand = [convert]::ToBase64String(([System.Text.Encoding]::Unicode.GetBytes($commandString))) $action = New-ScheduledTaskAction -Execute powershell.exe -Argument "-NoProfile -EncodedCommand $encodedCommand" if ($accessUserName -ne "SYSTEM") { $principal = New-ScheduledTaskPrincipal -UserId $accessUserName -LogonType Interactive } else { $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType Interactive } $taskItem = New-ScheduledTask -Action $action -Principal $principal -Description "Temporary Task" $parametersRegister = @{ TaskName = "TempTask_$([guid]::NewGuid())" InputObject = $taskItem } if ($accessUserName -ne "SYSTEM") { $parametersRegister["User"] = $AccessAccount.UserName $parametersRegister["Password"] = $AccessAccount.GetNetworkCredential().Password } $null = Register-ScheduledTask @parametersRegister #endregion Create Task #region Perform Encryption [System.IO.File]::WriteAllText($credFile, $Credential.GetNetworkCredential().Password) Start-ScheduledTask -TaskName $parametersRegister.TaskName Start-Sleep -Seconds 5 #endregion Perform Encryption #region Cleanup Unregister-ScheduledTask -TaskName $parametersRegister.TaskName -Confirm:$false if (Test-Path -Path $credFile) { try { Remove-Item $credFile -Force -ErrorAction Stop } catch { Write-Warning "[$env:COMPUTERNAME] Clear Text Credential File still exists!! $credFile | $_" } } if (-not (Test-Path -Path $file)) { throw "[$env:COMPUTERNAME] Failed to create credential file! ($file)" } #endregion Cleanup } } } function Send-MDMail { <# .SYNOPSIS Queues current email for delivery. .DESCRIPTION Uses the data prepared by Set-MDMail or Add-MDMailContent and queues the email for delivery. .PARAMETER TaskName Name of the task that is sending the email. Used in the name of the file used to queue messages in order to reduce likelyhood of accidental clash. .EXAMPLE PS C:\> Send-MDMail -TaskName "Logrotate" Queues the currently prepared email under the name "Logrotate" #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [string] $TaskName ) begin { # Ensure the pickup patch exists if (-not (Test-Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath'))) { try { $null = New-Item -Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath') -ItemType Directory -Force -ErrorAction Stop } catch { Stop-PSFFunction -String 'Send-MDMail.Folder.CreationFailed' -StringValues (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath') -ErrorRecord $_ -Cmdlet $PSCmdlet -EnableException $true } } } process { # Don't send an email if nothing was set up if (-not $script:mail) { Stop-PSFFunction -String 'Send-MDMail.Email.NotRegisteredYet' -EnableException $true -Cmdlet $PSCmdlet } $script:mail['Taskname'] = $TaskName # Send the email Write-PSFMessage -String 'Send-MDMail.Email.Sending' -StringValues $TaskName -Target $TaskName try { [PSCustomObject]$script:mail | Export-Clixml -Path "$(Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath')\$($TaskName)-$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').clixml" -ErrorAction Stop } catch { Stop-PSFFunction -String 'Send-MDMail.Email.SendingFailed' -StringValues $TaskName -ErrorRecord $_ -Cmdlet $PSCmdlet -EnableException $true -Target $TaskName } # Reset email, now that it is queued $script:mail = $null try { Start-ScheduledTask -TaskName MailDaemon -ErrorAction Stop } catch { Stop-PSFFunction -String 'Send-MDMail.Email.TriggerFailed' -StringValues $TaskName -ErrorRecord $_ -Cmdlet $PSCmdlet -EnableException $true -Target $TaskName } } } function Set-MDDaemon { <# .SYNOPSIS Configures the Daemon settings on the target computer(s) .DESCRIPTION Command that governs the Mail Daemon settings. .PARAMETER PickupPath The folder in which emails are queued for delivery. .PARAMETER SentPath The folder in which emails that were successfully sent are stored for a specified time before being deleted. .PARAMETER MailSentRetention The time to keep successfully sent emails around. .PARAMETER SmtpServer The mailserver to use for sending emails. .PARAMETER SenderDefault The default email address to use as sender. This is used for mails queued by a task that did not specify a sender. .PARAMETER RecipientDefault Default email address to send the email to, if the individual script queuing the email does not specify one. .PARAMETER SenderCredentialPath The path to where the credentials file can be found, that should be used by the daemon. .PARAMETER ComputerName The computer(s) to work against. Defaults to localhost, but can be used to update the module settings across a wide range of computers. .PARAMETER Credential The credentials to use when connecting to computers. .EXAMPLE PS C:\> Set-MDDaemon -PickupPath 'C:\MailDaemon\Pickup' Updates the configuration to now pickup incoming emails from 'C:\MailDaemon\Pickup'. Will not move pending email jobs. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] [CmdletBinding()] param ( [string] $PickupPath, [string] $SentPath, [Timespan] $MailSentRetention, [string] $SmtpServer, [string] $SenderDefault, [string] $RecipientDefault, [string] $SenderCredentialPath, [Parameter(ValueFromPipeline = $true)] [PSFComputer[]] $ComputerName = $env:COMPUTERNAME, [PSCredential] $Credential ) begin { #region Configuration Script $configurationScript = { param ( $Parameters ) # Import module so settings are initialized if (-not (Get-Module MailDaemon)) { Import-Module MailDaemon } foreach ($key in $Parameters.Keys) { Write-PSFMessage -String 'Set-MDDaemon.UpdateSetting' -StringValues $key, $Parameters[$key] switch ($key) { 'PickupPath' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.MailPickupPath' -Value $Parameters[$key] if (-not (Test-Path $Parameters[$key])) { $null = New-Item $Parameters[$key] -Force -ItemType Directory } } 'SentPath' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.MailSentPath' -Value $Parameters[$key] if (-not (Test-Path $Parameters[$key])) { $null = New-Item $Parameters[$key] -Force -ItemType Directory } } 'MailSentRetention' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.MailSentRetention' -Value $Parameters[$key] } 'SmtpServer' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.SmtpServer' -Value $Parameters[$key] } 'SenderDefault' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.SenderDefault' -Value $Parameters[$key] } 'SenderCredentialPath' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.SenderCredentialPath' -Value $Parameters[$key] } 'RecipientDefault' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.RecipientDefault' -Value $Parameters[$key] } } } Get-PSFConfig -Module MailDaemon -Name Daemon.* | Where-Object Unchanged -EQ $false | Register-PSFConfig -Scope FileSystem } #endregion Configuration Script #region Prepare parameters to pass through $parameters = @{ } foreach ($key in $PSBoundParameters.Keys) { if ($key -in 'ComputerName', 'Credential') { continue } $parameters[$key] = $PSBoundParameters[$key] } #endregion Prepare parameters to pass through } process { #region Modules must be installed and current if ($moduleResult = Test-Module -ComputerName $ComputerName -Credential $Credential -Module @{ MailDaemon = $script:ModuleVersion PSFramework = (Get-Module -Name PSFramework).Version } | Where-Object Success -EQ $false) { Stop-PSFFunction -String 'General.ModuleMissing' -StringValues ($moduleResult.ComputerName -join ", ") -EnableException $true -Cmdlet $PSCmdlet } #endregion Modules must be installed and current Write-PSFMessage -String 'Set-MDDaemon.UpdatingSettings' -StringValues ($ComputerName -join ", ") Invoke-PSFCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock $configurationScript -ArgumentList $parameters } } function Set-MDMail { <# .SYNOPSIS Changes properties for the upcoming mail to queue. .DESCRIPTION This command sets up the email to send, configuring properties such as the sender, recipient or content. .PARAMETER From The email address of the sender. .PARAMETER To The email address to send to. .PARAMETER Cc Additional addresses to keep in the information flow. .PARAMETER Subject The subject to send the email under. .PARAMETER Body The body of the email to send. You can individually add content to the body using Add-MDMailContent. .PARAMETER BodyAsHtml Whether the body is to be understood as html text. .PARAMETER Attachments Any attachments to send. Avoid sending large attachments with emails. You can individually add attachments to the email using Add-MDMailContent (using this parameter will replace attachments sent). .PARAMETER RemoveAttachments After sending the email, remove the attachments sent. Use this to have the system clean up temporary files you wrote before sending this report. .PARAMETER NotBefore Do not send this email before this timestamp has come to pass. .EXAMPLE PS C:\> Set-MDMail -From 'script@contoso.com' -To 'support@contoso.com' -Subject 'Daily Update Report' -Body $body Sends an email as script@contoso.com to support@contoso.com, reporting on the daily update status. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( [string] $From, [string] $To, [string[]] $Cc, [string] $Subject, [string] $Body, [switch] $BodyAsHtml, [string] $Attachments, [switch] $RemoveAttachments, [datetime] $NotBefore ) begin { if (-not $script:mail) { $script:mail = @{ } } } process { if ($From) { $script:mail["From"] = $From } if ($To) { $script:mail["To"] = $To } if ($Cc) { $script:mail["Cc"] = $Cc } if ($Subject) { $script:mail["Subject"] = $Subject } if ($Body) { $script:mail["Body"] = $Body } if ($BodyAsHtml.IsPresent) { $script:mail["BodyAsHtml"] = ([bool]$BodyAsHtml) } if ($Attachments) { $script:mail["Attachments"] = $Attachments } if ($RemoveAttachments.IsPresent) { $script:mail["RemoveAttachments"] = ([bool]$RemoveAttachments) } if ($NotBefore) { $script:mail["NotBefore"] = $NotBefore } } } function Update-MDFolderPermission { <# .SYNOPSIS Assigns permissions for the mail daemon working folders. .DESCRIPTION Assigns permissions for the mail daemon working folders. Enables simple assignment of privileges in case regular accounts need to write to protected pickup paths and helps implementing least privilege. .PARAMETER ComputerName The computer(s) to work against. Defaults to localhost. .PARAMETER Credential The credentials to use when connecting to computers. .PARAMETER DaemonUser The user to grant the necessary access to manage submitted mail items. .PARAMETER WriteUser Users that should be able to submit mails. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Update-MDFolderPermission -DaemonUser 'domain\srv_server1mail$' Grants Daemon User privileges on the local computer to the service account 'domain\srv_server1mail$' #> [CmdletBinding(SupportsShouldProcess = $true)] Param ( [Parameter(ValueFromPipeline = $true)] [PSFComputer[]] $ComputerName = $env:COMPUTERNAME, [PSCredential] $Credential, [string] $DaemonUser = " ", [string[]] $WriteUser = " " ) begin { #region Permission Assigning Scriptblock $permissionScript = { param ( [string] $DaemonUser, [string[]] $WriteUser ) Import-Module MailDaemon $pickupPath = (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath') $sentPath = (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailSentPath') if ($DaemonUser.Trim()) { Write-PSFMessage -String 'Update-MDFolderPermission.Granting.DaemonUser' -StringValues $DaemonUser, $pickupPath, $sentPath $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($DaemonUser, 'Read, Write', 'Allow') $acl = Get-Acl -Path $pickupPath $acl.AddAccessRule($rule) $acl | Set-Acl -Path $pickupPath $acl = Get-Acl -Path $sentPath $acl.AddAccessRule($rule) $acl | Set-Acl -Path $sentPath } foreach ($user in $WriteUser) { if ($user.Trim()) { continue } Write-PSFMessage -String 'Update-MDFolderPermission.Granting.WriteUser' -StringValues $user, $pickupPath $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($user, 'Write', 'Allow') $acl = Get-Acl -Path $pickupPath $acl.AddAccessRule($rule) $acl | Set-Acl -Path $pickupPath } } #endregion Permission Assigning Scriptblock } process { #region Modules must be installed and current if ($moduleResult = Test-Module -ComputerName $ComputerName -Credential $Credential -Module @{ MailDaemon = $script:ModuleVersion PSFramework = (Get-Module -Name PSFramework).Version } | Where-Object Success -EQ $false) { Stop-PSFFunction -String 'General.ModuleMissing' -StringValues ($moduleResult.ComputerName -join ", ") -EnableException $true -Cmdlet $PSCmdlet } #endregion Modules must be installed and current if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target ($ComputerName -join ", ") -Action "Granting the write permissions needed by the Daemon User ($($DaemonUser)) and Write User ($($WriteUser -join ', '))") { Invoke-PSFCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock $permissionScript -ArgumentList $DaemonUser, $WriteUser } } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'MailDaemon' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'MailDaemon' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'MailDaemon' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." Set-PSFConfig -Module 'MailDaemon' -Name 'Task.Author' -Value "$($env:USERDOMAIN) IT Department" -Initialize -Validation 'string' -SimpleExport -Description 'When setting up the scheduled task using Install-MDDaemon, this is the name used as the author of the scheduled task' Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.MailPickupPath' -Value "$($env:ProgramData)\PowerShell\MailDaemon\Pickup" -Initialize -Validation 'string' -SimpleExport -Description "The folder from which the daemon will pickup email tasks." Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.MailSentPath' -Value "$($env:ProgramData)\PowerShell\MailDaemon\Sent" -Initialize -Validation 'string' -SimpleExport -Description "The folder into which completed tasks are moved" Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.MailSentRetention' -Value (New-TimeSpan -Days 7) -Initialize -Validation 'timespan' -SimpleExport -Description "How long sent email tasks are retented" Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.SmtpServer' -Value "mail.$($env:USERDNSDOMAIN)" -Initialize -Validation 'string' -SimpleExport -Description "The mail server to use." Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.SenderDefault' -Value "maildaemon@$($env:USERDNSDOMAIN)" -Initialize -Validation 'string' -SimpleExport -Description "The default sending email address." Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.SenderCredentialPath' -Value '' -Initialize -Validation 'string' -SimpleExport -Description "The path to the credentials to use for authenticated mail sending." Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.RecipientDefault' -Value "support@$($env:USERDNSDOMAIN)" -Initialize -Validation 'string' -SimpleExport -Description "The default recipient to receive emails." <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'MailDaemon.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "MailDaemon.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name MailDaemon.alcohol #> New-PSFLicense -Product 'MailDaemon' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-02-08") -Text @" Copyright (c) 2019 Friedrich Weinmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ #endregion Load compiled code |