1

I have the following Powershell code which should send an email in the background and should wait till that email with the special attribute was also sent successfully. All works fine, but for any reason I cannot release the COM-object $sent after I used it in a loop or pipe. As a result the background process does not terminate as expected. How can I solve that problem?

Here the code-snippet:

cls
remove-variable * -ea 0
$ErrorActionPreference = 'stop'

# connect to existing outlook or start a new instance:
$i = [System.Runtime.Interopservices.Marshal]
Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook"
try {
    $o = $outlook = $i::GetActiveObject('Outlook.Application')
} catch {$o = New-Object -ComObject Outlook.Application}

# create email object:
$m = $o.CreateItem(0)

# set email properties:
$m.Subject = "Test"
$m.Body = "This is a test."
$m.To = "someone@somewhere.local"

# add a unique id to the mail:
$guid = [System.Guid]::NewGuid().guid
$id = $m.UserProperties.Add("guid",1)
$id.Value = $guid
$null = $i::ReleaseComObject($id)

# send the email and release the object:
$m.Send()
$null = $i::ReleaseComObject($m)

if ($outlook) {
    # no need to wait here, because outlook is still running:
    write-host "sending email initiated."
} else {
    # wait till email with that id appears in "sent" (or timeout):
    $timeout = 30
    $sent = $o.Session.GetDefaultFolder(5).Items
    do {
        sleep 1
        $sent.Sort("[SentOn]", $true)

        # this loop gets the id of the last sent mail,
        # but it blocks the release of $sent COM-object later.
        # same issue when using a pipe with "select -first 1"

        foreach($s in $sent){
            $id = $s.UserProperties['guid'].value
            break
        }
        $timeout -= 1
    } until ($timeout -eq 0 -or $id -eq $guid)

    # this does NOT release the object because I used it in the loop before:
    $null = $i::ReleaseComObject($sent)

    if ($id -eq $guid) {write-host "email sent."} 
    write-host "closing background process."
    $o.Quit()
}

# release the outlook object
$null = $i::ReleaseComObject($o)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
Carsten
  • 1,612
  • 14
  • 21
  • 2
    Releasing references to COM objects for out-of-process COM servers such as `Excel.Application` requires calling their `.Quit()` method first. However, the process doesn't exit until all references to the application object _as well as to elements of its document model_ have been released. While you _can_ call `[Runtime.InteropServices.Marshal]::ReleaseComObject()` on every reference, it is simpler to execute all COM operations in a _child scope_ (`& { ... }`) so that when its variables go out of scope, releasing happens automatically. You can speed up release by calling `[GC]::Collect()`. – mklement0 Jul 18 '23 at 15:13
  • How are you determining the object references aren't released, and what is `$null = $i::ReleaseComObject($s)` doing in the loop? – Mathias R. Jessen Jul 18 '23 at 15:13
  • This is not a duplicate, because the issue is related to the loop in the code. The quit-command is not the issue and I can confirm that releasing a COM object before the quit is also an option. If you rem-out the foreach-loop all is runnign fine. – Carsten Jul 18 '23 at 17:18
  • releasing the $s was was more or less a test. had no impact to the issue, that $sent cannot be released. – Carsten Jul 18 '23 at 17:20
  • I cannot test with Outlook myself, but see if enclosing everything between `$m = $o.CreateItem(0)` and the last `}` in `& { ... }` as well as moving `$o.Quit()` to after this new enclosure helps. – mklement0 Jul 18 '23 at 21:08
  • 1
    @mklement0 The trick with the child scope did it! It seems that the loop (or a pipe) creates a hidden dependency to the $sent object which cannot manually be cleaned up later. – Carsten Jul 19 '23 at 08:38

1 Answers1

0

Based on the good comments here the final solution to successfully close all COM-objects which also terminates the Outlook background process at the end:

cls
remove-variable * -ea 0
$ErrorActionPreference = 'stop'

# connect to existing outlook or start a new instance:
$i = [System.Runtime.Interopservices.Marshal]
Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook"
try {
    $o = $outlook = $i::GetActiveObject('Outlook.Application')
} catch {$o = New-Object -ComObject Outlook.Application}

# create email object:
$m = $o.CreateItem(0)

# set email properties:
$m.Subject = "Test"
$m.Body = "This is a test."
$m.To = "someone@somewhere.local"

# add a unique id to the mail:
$guid = [System.Guid]::NewGuid().guid
$id = $m.UserProperties.Add("guid",1)
$id.Value = $guid
$null = $i::ReleaseComObject($id)

# send the email and release the object:
$m.Send()
$null = $i::ReleaseComObject($m)

if ($outlook) {
    # no need to wait here, because outlook is still running:
    write-host "sending email initiated."
} else {
    # start child scope:
    & {
        # wait till email with that id appears in "sent" (or timeout):
        $timeout = 30
        $sent = $o.Session.GetDefaultFolder(5).Items
        do {
            sleep 1
            $sent.Sort("[SentOn]", $true)
            foreach($s in $sent){
                $id = $s.UserProperties['guid'].value
                break
            }
            $timeout -= 1
        } until ($timeout -eq 0 -or $id -eq $guid)
        if ($id -eq $guid) {write-host "email sent."}

    # end of child scope releases all COM dependencies
    }

    write-host "closing background process."
    $o.Quit()
}

# release the outlook object
$null = $i::ReleaseComObject($o)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
Carsten
  • 1,612
  • 14
  • 21