Skip to content

Add -MailType, fix configuration defaults, and dispatch through a service table - #13

Merged
Sekers merged 10 commits into
developfrom
refactor/service-dispatch
Sep 21, 2026
Merged

Sekers merged 10 commits into
developfrom
refactor/service-dispatch

Conversation

@Sekers

@Sekers Sekers commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Summary

Combines three stacked branches (configuration-default fixes, boolean-setting fixes, and the service-dispatch
refactor) into one pull request, and adds -MailType. The user-facing changes are in CHANGELOG.md under
[Unreleased]:

  • New Send-ScriptMessage -MailType: Group sends one email to all recipients, and OneOnOne sends each To,
    CC, and BCC recipient their own email with only them in To.
  • An explicit -ChatType OneOnOne or -IncludeBCCInGroupChat $false is now honored when the configuration file
    says otherwise.
  • A Mail send no longer needs a ChatType setting in the configuration file.
  • Get-ScriptMessageContext -ReturnCachedContext no longer reports a connection that Disconnect-ScriptMessage
    already ended.
  • Configuration file settings are validated more consistently, and several help topics are corrected.

Internal changes with no user-visible effect:

  • The public cmdlets reach a messaging service through a service table instead of per-service switch blocks, and
    a send reads the configuration file once.
  • Boolean configuration settings are read as opt-in flags (rule in AGENTS.md).
  • Tests/FileEncoding.Tests.ps1 enforces the file encoding rules, AGENTS.md gains a Testing section, and
    RELEASING.md requires a merge commit when merging main back into develop.
  • Every placeholder address uses an RFC 2606 reserved domain.

No live Microsoft Graph testing was done: every test mocks the service functions or the Graph cmdlets. 79 tests
pass under Windows PowerShell 5.1 and PowerShell 7.

Release classification

  • Major: breaks the public contract (an existing script or configuration file can stop working)
  • Minor: adds backward-compatible functionality
  • Patch: a backward-compatible fix
  • None: no user-visible change (tests, CI, or contributor documentation only)

Checklist

  • Targets develop, or main for a hotfix
  • CHANGELOG.md has an entry in the right section under [Unreleased] (added if missing), or the change is not user-visible
  • .\Tests\Invoke-Tests.ps1 passes
  • Contains no real tenant names, email domains, secrets, or configuration files
  • Any live Microsoft Graph testing was done in a test tenant and is described above

- The defaults block tested the parameters for truthiness, so an explicit
  -ChatType OneOnOne or -IncludeBCCInGroupChat $false was replaced by the
  configuration file's value. OneOnOne is ChatType enum value 0 and $false is
  a valid choice, so neither can be told apart from an unsupplied value that
  way. Test $PSBoundParameters.ContainsKey() instead.
- The same block cast the configuration value to [ChatType] unconditionally,
  so a configuration file with no ChatType setting made every send throw
  "Cannot convert null to type ChatType", including a Mail-only send that has
  no use for a chat type.
- Resolve both settings into separate $Resolved variables. A parameter keeps
  its type constraint through later assignments, so [ChatType] cannot be
  reassigned the $null of a missing setting. This also stops the values
  leaking between services in a multi-service call, since each iteration now
  derives them from its own service configuration.
- Add ChatType to the Send-ScriptMessage_MicrosoftGraph splat only when
  [string]::IsNullOrWhiteSpace says there is a real value. Neither $null nor
  the empty string a configuration file uses for an unset setting can bind to
  an enum parameter.
- Report a Chat message that arrived without a chat type from
  Send-ScriptMessage_MicrosoftGraph, after its AllowableMessageTypes check, as
  a "Chat not sent." warning in the result's Error property. Raising it from
  Send-ScriptMessage instead preempts that check and tells a Mail-only
  configuration to add ChatType when what it actually needs is Chat in
  AllowableMessageTypes, and every other chat failure is already reported this
  way rather than thrown.
- Add Pester coverage for both parameters against a configuration that
  disagrees, the configuration-default path, a Mail-only send with no ChatType
  setting, a Chat send with no chat type anywhere, a blank ChatType setting,
  and a Mail-only configuration asked to send Chat, which must still name the
  disallowed type. The existing chat tests now pass -ChatType rather than
  setting it in the mocked configuration.
- Record the enum and type-constraint measurements behind the fix in
  Research_Notes/PowerShell-Language-Behavior.md section 10.
- AGENTS.md and CONTRIBUTING.md required placeholder addresses on
  domain.com, which is a real registered domain. Require a domain reserved
  by RFC 2606 instead: example.com, example.net, or example.org. None of
  those can ever be registered, so a placeholder copied out of a help
  example and run unedited, or a test whose mock is missing, cannot reach a
  real recipient. The help examples were the weakest spot, since those are
  what a reader copies and runs.
- Send-ScriptMessage help: 14 example addresses to example.com.
- Research_Notes/PowerShell-Language-Behavior.md section 9 records observed
  output, so the expressions were re-run with the new address in both
  editions rather than edited by hand.
- Tests: the one placeholder URL in the attachment mock.
- The rule states example.com as the preference but all three as equally
  correct, so nobody renames the test suite's existing example.org
  addresses to match.
- [bool] of any non-empty string is $true, so a quoted "false" meant its
  opposite. IncludeBCCInGroupChat was read with a [bool] cast and
  MgDisconnectWhenDone with a truthiness test, so both inverted. The two
  MgDelegatedPermission_* settings are read as -eq $true, which compares as
  strings and is already correct.
- Declare -IncludeBCCInGroupChat as [Nullable[bool]] instead of [Object] with
  [ValidateSet($null, $true, $false)]. The binder refuses a string outright,
  while the validation set accepted 'false' and left it a string for the later
  cast to turn into $true. Nullable keeps the three states the parameter needs,
  so $null still means "use the configured value", and an unsupplied parameter
  holds $null too, which retires the $PSBoundParameters.ContainsKey() test.
- A configuration file value never passes through a binder, so add
  Get-ScriptMessageBooleanSetting to reject anything -isnot [bool] with an error
  naming the setting. Giving that helper a [Nullable[bool]] parameter would
  delegate the check to the binder but report the internal parameter name
  instead of the setting, which is no help to someone holding a JSON file.
- Each layer checks the settings it owns, before anything is sent:
  Send-ScriptMessage for the generic IncludeBCCInGroupChat, and
  Send-ScriptMessage_MicrosoftGraph for MgDisconnectWhenDone. That means moving
  the Disconnect-MgGraph teardown into the service as well, so the layer that
  validates the setting is the one that consumes it.
- Add Pester coverage for a string passed to the parameter, a quoted value for
  each setting, and that neither sends. The success cases assert Send-MgUserMail
  was invoked, because an earlier version of these tests built its config mocks
  with GetNewClosure(), which swallowed Pester's injected $Service and passed
  without sending anything.
- Record the binder, cast, class property and ValidateSet differences in
  Research_Notes/PowerShell-Language-Behavior.md section 10.
- Get-ScriptMessageContext called Get-MgContext directly, so a core cmdlet
  depended on Microsoft.Graph.Authentication. Add
  Get-ScriptMessageContext_MicrosoftGraph alongside the existing
  Disconnect-ScriptMessage_MicrosoftGraph and call that instead. No cmdlet in
  ScriptMessage/Public/ calls a Microsoft Graph cmdlet now.
- Lift the property-merging loop out of the switch arm so it runs on whatever
  any service returns, which also retires the "break # Terminate the switch
  statement" that only existed because the emptiness check sat inside the arm.
  Keep [string]::IsNullOrEmpty for that check so the test is unchanged from
  what shipped.
- Add Pester coverage for this cmdlet, which had none: the service name is
  returned alongside the service's own properties, a service with no
  connection yields just the service name, and -ReturnCachedContext does not
  ask the service twice. One test mocks Get-MgContext rather than the service
  function, so it covers the whole chain.

Output is unchanged, so there is no changelog entry.
- Disconnect-ScriptMessage left ScriptMessage_Global_CachedServiceContext
  untouched, so Get-ScriptMessageContext -ReturnCachedContext kept returning
  the account and scopes of a session that had already ended. Remove the
  service's entry from the cache after the service disconnects, which makes
  the next cached read refresh instead of replaying it. Removing a name the
  cache does not hold is a no-op, so calling Disconnect-ScriptMessage before
  anything has asked for the context is unaffected.
- Add Pester coverage for Disconnect-ScriptMessage, which had none: it asks
  the service to disconnect, returns the service name alongside the service's
  own disconnection information under -ReturnConnectionInfo, returns nothing
  without that switch, and clears the cache. Mocking the service functions
  rather than the Microsoft Graph cmdlets keeps the file free of stand-ins.
- Each public cmdlet named MicrosoftGraph in a switch block, five in all, so
  adding a service meant editing four files in the core. Add a module-scope
  service table that each file in Services/ registers itself into as it is
  dot-sourced, and replace every switch with a lookup plus a call.
- Record handlers as function NAMES and resolve them with & at call time.
  Caching a CommandInfo or a script block at registration would bind to the
  real function and bypass Pester, which mocks by shadowing a command name.
- Register-ScriptMessageService rejects a name that is not a MessagingService
  member, since every -Service parameter is typed to that enum and a service
  outside it could never be reached. Get-ScriptMessageServiceRegistration
  throws naming the registered services, replacing a Default arm that said
  only "Invalid 'Service' value."
- Disconnect-ScriptMessage loses its second switch: the loop that merges the
  service's return information is service-agnostic, so it now runs on whatever
  comes back, which retires another "break # Terminate the switch statement".
- Add Pester coverage: the table holds the Graph handlers, an unknown service
  names the registered ones, a non-enum name is refused, and all four public
  cmdlets reach stand-in handlers swapped into the table with no Microsoft
  Graph mocks loaded, which is what proves the core reaches a service only
  through the table.

Output is unchanged, so there is no changelog entry.
- Send-ScriptMessage, Connect-ScriptMessage and Send-ScriptMessage_MicrosoftGraph
  each read the file, so one send read it three times, and five times for two
  services. Each read reopens the file, redoes the encoding fallback and
  reparses the JSON, so a send had no single consistent view of its own
  settings: editing the file mid-send could hand the connection different
  credentials than the checks that followed it.
- Send-ScriptMessage now reads once and passes the service's section to
  Connect-ScriptMessage and to the service through a new optional
  -ServiceConfig parameter. Both still read the file when nobody passes it, so
  calling either on its own is unchanged.
- Resolve the section with the registered service name rather than the
  -Service parameter's value, because looking a property up by a name held in
  an array comes back empty. A configuration file with no section for the
  service now says so instead of carrying $null into the connection.
- Add Pester coverage asserting one read across a Mail and Chat send, and that
  Connect-ScriptMessage on its own still reads the file.

No changelog entry: passing a configuration section was already possible
through the service functions, and -ServiceConfig is plumbing rather than a
feature. The wiki will cover it.
- -MailType was accepted and then ignored: the core never forwarded it, no
  service acts on it, and the Mail result reads an undefined variable into its
  MailType property. Services are meant to be interchangeable, so a setting a
  service cannot act on is an ordinary case, but dropping it without a word is
  not.
- Register-ScriptMessageService now requires -SupportedSetting, the generic
  unprefixed settings a service honors. Mandatory so that adding a service
  forces an answer. Microsoft Graph declares ChatType and
  IncludeBCCInGroupChat, and not MailType.
- Send-ScriptMessage warns when a caller supplies a generic setting the service
  does not declare, then sends anyway. A warning rather than an error because a
  single call can go to several services and only some may honor it. A $null is
  skipped, since that means "use the configured value".
- The generic defaults stay in the core rather than moving into the service, as
  the original plan had it. ChatType and IncludeBCCInGroupChat are generic
  vocabulary, so resolving them per service would duplicate the same merge rule
  in every service.
- Fix the .PARAMETER MailType help directive, whose trailing "# TODO:" comment
  stopped Get-Help matching it to the parameter, so Get-Help fell back to the
  code comment above the declaration and showed an internal note.
- -MailType and the configuration file's MailType setting were accepted and
  ignored. OneOnOne now sends each unique To, CC, and BCC address its own
  email with only that address in To; Group, the default, sends one email.
  Send-ScriptMessage resolves it like ChatType: the parameter beats the
  configuration file, a missing or blank setting means Group, and an
  unrecognized value throws before connecting. It is resolved only for a
  send that includes Mail, so it cannot stop a Chat send.
- The Graph service sends one request per recipient, deduplicating addresses
  without regard to case. A failure for one recipient goes into the result's
  Error property and the rest are still sent; -ErrorAction Stop makes the
  catch fire whether or not Send-MgUserMail's error is terminating
  (Research_Notes/Microsoft-Graph-Behavior.md section 3). The result's
  MailType property now holds the mail type used. Microsoft Graph declares
  MailType in -SupportedSetting, and ServiceDispatch.Tests.ps1 narrows the
  registration to keep exercising the unsupported-setting warning.
- Remove Get-ScriptMessageBooleanSetting. The 1.1.0 bug was two reads that
  turn a quoted "false" into true: a [bool] cast for IncludeBCCInGroupChat
  and a truthiness test for MgDisconnectWhenDone. Every boolean setting is
  now an opt-in flag read as <setting> -eq $true with the setting on the
  left, the form the MgDelegatedPermission_* settings already used, so any
  value other than true means off, the safe state for all four. The rule is
  in AGENTS.md and the measurements in
  Research_Notes/PowerShell-Language-Behavior.md section 10.
- Add Tests/FileEncoding.Tests.ps1, which enforces the encoding rules in
  AGENTS.md (no BOM except a manifest that needs one, LF only, ASCII-only
  PowerShell files) over tracked and untracked, unignored files, and replace
  the manual check snippet in AGENTS.md with a pointer to it.
- Add a Testing section to AGENTS.md: running Invoke-Tests.ps1, how the
  Pester files avoid a tenant, and that the scratch scripts in Tests/ send
  real messages.
- RELEASING.md: develop allows squash merges for feature branches, but main
  must be merged back into develop with a merge commit so develop keeps the
  tagged release commit.
- Changelog: add -MailType under Added, drop the entry for the -MailType
  warning (which never shipped) and the help-text entry, drop the specific
  quoted-boolean entries, and add a general Minor entry about validation.
- Register-ScriptMessageService's -SupportedSetting listed the generic
  settings a service honors, and Send-ScriptMessage warned when a caller
  passed one the service did not list. With MailType implemented, Microsoft
  Graph lists all three, so no real call could reach the warning.
- It controlled nothing: Send-ScriptMessage passes every message parameter
  to every service regardless, so a service's send function already has to
  declare each one. It also worked per service rather than per message type,
  covered 3 of about 15 parameters, and wrote only to the warning stream,
  where scripts that inspect results never see it.
- Replace it with a rule in AGENTS.md: a service that cannot honor a setting
  it receives adds a Warning to that message's Error property and sends what
  it can, as the Graph chat path already does for sending on behalf of
  others.
- Remove the tests that covered it and the narrowed registration they
  needed.

No changelog entry: the declaration and its warning never shipped.
@Sekers
Sekers merged commit a3c01df into develop Sep 21, 2026
4 checks passed
@Sekers
Sekers deleted the refactor/service-dispatch branch September 21, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant