Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ MailDaemon/MailDaemon.psproj
TestResults/*

# ignore the publishing Directory
publish/*
publish/*

experiments/*
3 changes: 2 additions & 1 deletion MailDaemon/MailDaemon.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
RootModule = 'MailDaemon.psm1'

# Version number of this module.
ModuleVersion = '1.2.14'
ModuleVersion = '1.3.19'

# ID used to uniquely identify this module
GUID = 'd5ba333f-5210-4d69-83f0-150dd0909139'
Expand All @@ -27,6 +27,7 @@
# this module
RequiredModules = @(
@{ ModuleName='PSFramework'; ModuleVersion='1.13.416' }
@{ ModuleName='EntraAuth'; ModuleVersion='1.8.52' }
)

# Assemblies that must be loaded prior to importing this module
Expand Down
8 changes: 8 additions & 0 deletions MailDaemon/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.3.19 (2026-05-28)

+ Major: Added capability to send emails by Graph API, rather than SMTP.
+ New: Configuration Setting 'MailDaemon.Daemon.Type' - determins whether to send email by Smtp or Graph.
+ New: Configuration Settings to define Graph behavior: 'MailDaemon.Daemon.Graph.*'
+ Upd: New dependency: EntraAuth - implements the Graph API authentication and interaction.
+ Upd: Mail Tasks are now stored in PSFramework CliDat format to save disk space. New client tasks cannot be processed by old agent versions.

## 1.2.14 (2026-02-12)

+ Fix: Update-MDFolderPermission - does not set permissions for the failed folder
Expand Down
5 changes: 5 additions & 0 deletions MailDaemon/en-us/strings.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
'General.ModuleMissing' = 'The MailDaemon module could not be found in sufficient version on: {0}. Terminating Execution. To install or update to the current version, use Install-MDDaemon or access the PSGallery directly using "Install-Module MailDaemon".'

# Invoke-MDDaemon
'Invoke-MDDaemon.Error.General' = 'An error happened while processing pending emails'
'Invoke-MDDaemon.SendMail.Abandon' = '{0} - Abandoning email after failing to send it within the configured timespan ({1})' # $email.Taskname, $abandonThreshold
'Invoke-MDDaemon.SendMail.Start' = '{0} - Sending Mail: "{1}" From {2} to {3}'
'Invoke-MDDaemon.SendMail.Failed' = '{0} - Failed to send email!'
Expand All @@ -30,4 +31,8 @@
# Update-MDFolderPermission
'Update-MDFolderPermission.Granting.DaemonUser' = 'Assigning write permissions as daemon account to {0} on "{1}" and "{2}"'
'Update-MDFolderPermission.Granting.WriteUser' = 'Assigning write permissions as mail submitter to {0} on "{1}"'

# Connect-MDGraph
'Connect-MDGraph.Error.NoAuthPath' = 'Failed to connect to Entra & Graph: None of the required authentication options were specified! Use Set-MDDaemon to configure a certificate or federated credetnials!'
'Connect-MDGraph.Error.NoClientIDorTenantID' = 'Failed to connect to Entra & Graph: ClientID or TenantID were missing and must be provided!'
}
44 changes: 42 additions & 2 deletions MailDaemon/functions/Install-MDDaemon.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@
.PARAMETER UseSSL
Use SSL for sending emails.

.PARAMETER ClientID
The ClientID of the Application to use for sending emails via Graph API.

.PARAMETER TenantID
The TenantID of the Application to use for sending emails via Graph API.

.PARAMETER Identity
When authenticating to Entra for sending emails via Graph API, use the current Managed Identity to authenticate.

.PARAMETER Federated
When authenticating to Entra for sending emails via Graph API, use Federated Credentials to authenticate.
This uses the current Managed Identity to get a token they can use to authenticate to the application with the actual permissions.

.PARAMETER CertificateThumbprint
When authenticating to Entra for sending emails via Graph API, use the certificate with the specified thumbprint.
The certificate must be stored in one of the local certificate stores.

.PARAMETER CertificateName
When authenticating to Entra for sending emails via Graph API, use the newest certificate with the specified subject.
The certificate must be stored in one of the local certificate stores.

.PARAMETER NoLogging
Disables logging.
Unless specified, this setup step will also prepare the windows eventlog by creating a dedicated eventlog for MailDaemon.
Expand Down Expand Up @@ -132,6 +153,24 @@
[switch]
$UseSSL,

[string]
$ClientID,

[string]
$TenantID,

[switch]
$Identity,

[switch]
$Federated,

[string]
$CertificateThumbprint,

[string]
$CertificateName,

[switch]
$NoLogging
)
Expand Down Expand Up @@ -210,7 +249,8 @@
#endregion Setup Task Configuration

#region Preparing Parameters
$parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include 'PickupPath', 'SentPath', 'FailedPath', 'MailSentRetention', 'MailAbandonThreshold', 'MailFailedRetention', 'SmtpServer', 'SenderDefault', 'RecipientDefault', 'UseSSL'
$parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include 'PickupPath', 'SentPath', 'FailedPath', 'MailSentRetention', 'MailAbandonThreshold', 'MailFailedRetention', 'SmtpServer', 'SenderDefault', 'RecipientDefault', 'UseSSL', 'ClientID', 'TenantID', 'Identity', 'Federated', 'CertificateThumbprint', 'CertificateName'
if ($parameters.Federated -or $parameters.Identity -or $parameters.ClientID) { $parameters.Type = 'Graph' }

$paramMainInstallCall = @{
ArgumentList = $parameters
Expand All @@ -227,7 +267,7 @@
Import-Module -Name PSFramework
Import-Module -Name MailDaemon

Set-MDDaemon @parameters
Set-MDDaemon @Parameters

#region Set file permissions
if (-not (Test-Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath'))) { $null = New-Item (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath') -Force -ItemType Directory }
Expand Down
39 changes: 33 additions & 6 deletions MailDaemon/functions/Invoke-MDDaemon.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,34 @@
if (-not $NoLogging -and ($PSVersionTable.PSVersion.Major -lt 6 -or $IsWindows)) {
Set-PSFLoggingProvider -Name eventlog -InstanceName MailDaemonInvoke -LogName MailDaemon -Source MailDaemon -Enabled $true -Wait
}

$type = Get-PSFConfigValue -FullName 'MailDaemon.Daemon.Type' -Fallback 'Smtp'
$useSmtp = 'Smtp' -eq $type
$useGraph = 'Graph' -eq $type
}
process {
trap {
Write-PSFMessage -Level Warning -String 'Invoke-MDDaemon.Error.General' -ErrorRecord $_
Disable-PSFLoggingProvider -Name eventlog -InstanceName MailDaemonInvoke
throw $_
}

if ($useGraph) { Connect-MDGraph }


#region Send mails
foreach ($item in (Get-ChildItem -Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath') -Filter "*.clixml")) {
$email = Import-Clixml -Path $item.FullName
$email = Import-PSFClixml -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 (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.UseSSL' -Fallback $false) { $parameters['UseSSL'] = $true }


#region General
if ($email.To) { $parameters["To"] = $email.To }
else { $parameters["To"] = Get-PSFConfigValue -FullName 'MailDaemon.Daemon.RecipientDefault' }
if ($email.From) { $parameters["From"] = $email.From }
Expand Down Expand Up @@ -91,10 +99,29 @@
$parameters["Attachments"] = $email.Attachments
}
}
if (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.SenderCredentialPath') { $parameters["Credential"] = Import-Clixml -Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.SenderCredentialPath') }
#endregion General

#region Smtp
if ($useSmtp) {
$parameters += @{
SmtpServer = Get-PSFConfigValue -FullName 'MailDaemon.Daemon.SmtpServer'
Encoding = [System.Text.Encoding]::UTF8
}
if (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.UseSSL' -Fallback $false) { $parameters['UseSSL'] = $true }
if (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.SenderCredentialPath') { $parameters["Credential"] = Import-Clixml -Path (Get-PSFConfigValue -FullName 'MailDaemon.Daemon.SenderCredentialPath') }

$sendCommand = Get-Command -Name Send-MailMessage
}
#endregion Smtp

#region Graph
if ($useGraph) {
$sendCommand = Get-Command -Name Send-GraphMail
}
#endregion Graph

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 }
try { & $sendCommand @parameters }
catch {
"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') : $_" | Set-PSFFileContent -Path ($item.FullName -replace '.clixml', '.txt') -Append
#region Abandon Email if beyond threshold
Expand Down
13 changes: 11 additions & 2 deletions MailDaemon/functions/Send-MDMail.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

.PARAMETER PersistAttachments
Attachments will be serialized with the queued email allowing the source files to be removed immediately.

.PARAMETER DontTrigger
Do not trigger the task that sends the email.
By default, after submitting an email for delivery, it will immediately trigger the scheduled task to send it.

.EXAMPLE
PS C:\> Send-MDMail -TaskName "Logrotate"
Expand All @@ -26,7 +30,10 @@
$TaskName,

[switch]
$PersistAttachments
$PersistAttachments,

[switch]
$DontTrigger
)

begin
Expand Down Expand Up @@ -60,7 +67,7 @@

# 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" -Depth 4 -ErrorAction Stop }
try { [PSCustomObject]$script:mail | Export-PSFClixml -Path "$(Get-PSFConfigValue -FullName 'MailDaemon.Daemon.MailPickupPath')\$($TaskName)-$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').clixml" -Depth 4 -ErrorAction Stop }
catch
{
Stop-PSFFunction -String 'Send-MDMail.Email.SendingFailed' -StringValues $TaskName -ErrorRecord $_ -Cmdlet $PSCmdlet -EnableException $true -Target $TaskName
Expand All @@ -69,6 +76,8 @@
# Reset email, now that it is queued
$script:mail = $null

if ($DontTrigger) { return }

try { Start-ScheduledTask -TaskName MailDaemon -ErrorAction Stop }
catch
{
Expand Down
61 changes: 59 additions & 2 deletions MailDaemon/functions/Set-MDDaemon.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@

.PARAMETER MailFailedRetention
How long we keep an abandoned email around before removing it entirely.

.PARAMETER Type
In what fundamental way should emails be sent?
- SMTP: Via classic SMTP relay (authenticated or not so)
- Graph: Via Graph API (using Application authentication)
The different modes need different configuration parameters.

.PARAMETER SmtpServer
The mailserver to use for sending emails.
Expand All @@ -39,6 +45,27 @@

.PARAMETER UseSSL
Use SSL for sending emails.

.PARAMETER ClientID
The ClientID of the Application to use for sending emails via Graph API.

.PARAMETER TenantID
The TenantID of the Application to use for sending emails via Graph API.

.PARAMETER Identity
When authenticating to Entra for sending emails via Graph API, use the current Managed Identity to authenticate.

.PARAMETER Federated
When authenticating to Entra for sending emails via Graph API, use Federated Credentials to authenticate.
This uses the current Managed Identity to get a token they can use to authenticate to the application with the actual permissions.

.PARAMETER CertificateThumbprint
When authenticating to Entra for sending emails via Graph API, use the certificate with the specified thumbprint.
The certificate must be stored in one of the local certificate stores.

.PARAMETER CertificateName
When authenticating to Entra for sending emails via Graph API, use the newest certificate with the specified subject.
The certificate must be stored in one of the local certificate stores.

.PARAMETER ComputerName
The computer(s) to work against.
Expand Down Expand Up @@ -74,6 +101,10 @@

[Timespan]
$MailFailedRetention,

[ValidateSet('Graph', 'Smtp')]
[string]
$Type,

[string]
$SmtpServer,
Expand All @@ -89,6 +120,24 @@

[switch]
$UseSSL,

[string]
$ClientID,

[string]
$TenantID,

[switch]
$Identity,

[switch]
$Federated,

[string]
$CertificateThumbprint,

[string]
$CertificateName,

[Parameter(ValueFromPipeline = $true)]
[PSFComputer[]]
Expand Down Expand Up @@ -131,6 +180,13 @@
'SenderCredentialPath' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.SenderCredentialPath' -Value $Parameters[$key] }
'RecipientDefault' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.RecipientDefault' -Value $Parameters[$key] }
'UseSSL' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.UseSSL' -Value $Parameters[$key].ToBool() }
'Type' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.Type' -Value $Parameters[$key] }
'ClientID' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.Graph.ClientID' -Value $Parameters[$key] }
'TenantID' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.Graph.TenantID' -Value $Parameters[$key] }
'Identity' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.Graph.Identity' -Value $Parameters[$key] }
'Federated' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.Graph.Federated' -Value $Parameters[$key] }
'CertificateThumbprint' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.Graph.CertificateThumbprint' -Value $Parameters[$key] }
'CertificateName' { Set-PSFConfig -Module MailDaemon -Name 'Daemon.Graph.CertificateName' -Value $Parameters[$key] }
}
}

Expand All @@ -139,10 +195,11 @@
#endregion Configuration Script

$parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude ComputerName, Credential
$connect = $PSBoundParameters | ConvertTo-PSFHashtable -Include ComputerName, Credential
}
process {
#region Modules must be installed and current
if ($moduleResult = Test-Module -ComputerName $ComputerName -Credential $Credential -Module @{
if ($moduleResult = Test-Module @connect -Module @{
MailDaemon = $script:ModuleVersion
PSFramework = (Get-Module -Name PSFramework).Version
} | Where-Object Success -EQ $false) {
Expand All @@ -151,6 +208,6 @@
#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
Invoke-PSFCommand @connect -ScriptBlock $configurationScript -ArgumentList $parameters
}
}
17 changes: 15 additions & 2 deletions MailDaemon/internal/configurations/daemon.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@ Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.MailFailedPath' -Value "$(Get-P
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.MailSentRetention' -Value (New-TimeSpan -Days 7) -Initialize -Validation 'timespan' -SimpleExport -Description "How long sent email tasks are retained"
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.MailAbandonThreshold' -Value (New-TimeSpan -Days 14) -Initialize -Validation 'timespan' -SimpleExport -Description "How long we try to send failing tasks, before abandoning them"
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.MailFailedRetention' -Value (New-TimeSpan -Days 14) -Initialize -Validation 'timespan' -SimpleExport -Description "How long failed email tasks are retained"
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.Type' -Value 'SMTP' -Initialize -Validation 'MailDaemon.Protocol' -SimpleExport -Description 'The protocol used to send email. Supports either "SMTP" or "Graph". The choice determines what authentication options must be specified.'

# SMTP
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.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."
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.UseSSL' -Value $false -Initialize -Validation 'bool' -SimpleExport -Description "Whether mails should be sent using SSL."
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.UseSSL' -Value $false -Initialize -Validation 'bool' -SimpleExport -Description "Whether mails should be sent using SSL."

# Graph
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.Graph.ClientID' -Value $null -Initialize -Validation guid -SimpleExport -Description 'The client ID of the Application to use for authentication to graph.'
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.Graph.TenantID' -Value $null -Initialize -Validation guid -SimpleExport -Description 'The tenant ID of the Application to use for authentication to graph.'
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.Graph.CertificateThumbprint' -Value '' -Initialize -Validation string -SimpleExport -Description 'Authenticate using the specified certificate (by thumbprint). The account must have read access to the private key.'
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.Graph.CertificateName' -Value '' -Initialize -Validation string -SimpleExport -Description 'Authenticate using the specified certificate (by subject). The account must have read access to the private key.'
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.Graph.Federated' -Value $false -Initialize -Validation bool -SimpleExport -Description 'Authenticate using Federated Credentials. Generally requires running as admin for access.'
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.Graph.Identity' -Value $false -Initialize -Validation bool -SimpleExport -Description 'Authenticate using the Managed Identity of the current environment. Generally requires running as admin for access.'
Set-PSFConfig -Module 'MailDaemon' -Name 'Daemon.Graph.NoAuth' -Value $false -Initialize -Validation bool -SimpleExport -Description 'Do not authenticate at all during processing. This assumes you have handled graph authentication before calling Invoke-MDDaemon - something the default task the module sets up will not do. Use with care'
Loading
Loading