Introduction
In the automation world, organizations are increasingly relying on Microsoft 365 for their email infrastructure. In this era of automation, it is very crucial to automate email management. Recently, I encountered an issue while developing a PowerShell script for email backup for the given users' OneDrive. The script involves deleting attachments from the mailbox, restoring them, and adding links to the mailbox with a folder URL to redirect to the actual attachment storage location. This script turned from simple automation to a deep dive into Microsoft Graph api concurrency handling.
In this blog, we will explore the reasons behind the concurrency issue and why PowerShell cmdlets fail, while the Graph REST API succeeds.
Script goal
- Ask the user to select user type, comma-separated or all users.
- Then ask the user for the date range between emails received.
- Ask the user for the folder name from Outlook.
- After these questions the script runs and does below tasks.
- Check all emails for any attachments, such as images, PDFs, zips, and more.
- If there is, it uploads those attachments to the user’s OneDrive in a timestamp-titled folder, also storing the mail URL in that folder to access the mail from OneDrive easily.
- Then, it updates each email with its attachment links, which are stored in OneDrive, and also adds a folder link to redirect.
- Then delete those attachments from the mail.
That’s it, this was the challenge that I was achieving. Script runs fine, but sometimes it gives me errors like shown in the image below.
![Powershell]()
I found a lot, then I got some fixes, and now I'm going to share them with you.
Using the PowerShell cmdlet Remove-MgUserMessageAttachment generates ETag and concurrency conflicts.
Let’s understand ETag and concurrency.
The send or update operation could not be performed because the change key passed in the request does not match the current change key for the item., Cannot save changes made to an item to store. SaveStatus:
- I IrresolvableConflict PropertyConflicts: Status: 412
- I (PreconditionFailed) ErrorCode: ErrorIrresolvableConflict
The above error statement says about the key.
ETag (Entity Tag): It is one type of versioning mechanism that REST API uses to handle concurrent modifications, as shown below, for example.
- When you fetch a message, it comes with an ETag, similar to a version number.
- When you modify that message, the ETag changes.
- Any further operation must use the current ETag, which frequently changes.
- When you try to get the old ETag, you get this error with Status 412.
Below is the command that was causing the issue.
# Step 1: Get message (ETag: "v1")
$message = Get-MgUserMessage -UserId $userId -MessageId $messageId
# Step 2: Update message body with attachment links (ETag becomes "v2")
Update-MgUserMessage -UserId $userId -MessageId $messageId -Body $newBody
# Step 3: Try to delete attachment using old ETag ("v1") - FAILS!
Remove-MgUserMessageAttachment `
-UserId $UserId `
-MessageId $Message.Id `
-AttachmentId $currentItemName `
-IfMatch (Get-MgUserMessage -UserId $UserId -MessageId $Message.Id).AdditionalProperties["@odata.etag"]
The Solution: Direct use of REST API Calls.
Let's see why these direct REST API Calls work better and avoid those errors.
Instead of relying on PowerShell cmdlets, I used the below function, which makes REST API calls using Invoke-MgGraphRequest - delete method.
function Remove-AttachmentWithRestAPI {
param (
$UserId,
$MessageId,
$AttachmentId,
$AttachmentName
)
for ($attempt = 1; $attempt -le 5; $attempt++) {
try {
# Making direct REST API calls
$deleteUri = "https://graph.microsoft.com/v1.0/users/$UserId/messages/$MessageId/attachments/$AttachmentId"
# Use Invoke-MgGraphRequest with DELETE method
$result = Invoke-MgGraphRequest -Method DELETE -Uri $deleteUri
Write-LogToFile "✅ SUCCESS: Deleted $AttachmentName (attempt $attempt)"
return $true
}
catch {
$errorMessage = $_.Exception.Message
Write-LogToFile "⚠️ Attempt $attempt failed for $AttachmentName : $errorMessage"
if ($attempt -lt 5) {
$waitTime = $attempt * 3 # Wait 3s, 6s, 9s, 12s, 15s
Write-LogToFile "Waiting $waitTime seconds before retry..."
Start-Sleep -Seconds $waitTime
}
}
}
Write-LogToFile "❌ FINAL FAILURE: Could not delete $AttachmentName after 5 attempts"
return $false
}
The above function works. It checks for failed API calls, then goes to catch and waits for a few times before running again, making it more robust for deletion without error.
The key advantage of this approach is listed below.
- There is no requirement for ETag because it makes direct REST API Calls.
- If there is a failure in the API Call, then it runs again after a few seconds.
- More granular control over error responses.
- Handles throttling more gracefully.
Conclusion
This script taught me that while using PowerShell cmdlets provides convenience, they abstract some essential details that we indeed need in production time. When dealing with complex API operations, as seen in the example above. Where concurrency and state management are involved, a direct REST API Call provides more robust control and readability. If you are working with the Microsoft Graph API in a production environment, I recommend using direct REST API calls via Invoke-MgGraphRequest for critical operations, especially those that need frequent modifications.