none
Mail.Move in Exchange 2013 via Powershell and EWS RRS feed

  • Question

  • Hi,
    after a Migration from Notes to Exchange 2013 on-premise, one Mailbox got migrated falsely: All mails, which had been sorted in a number of Folders in the original Mailbox, where sorted to a Folder "All documents". :(
    Since this issue was remarked after 3 month, there is no Chance, to replace the Mailbox with a newly migrated one.

    So the following plan: We have another Version of the Mailbox on Exchange which was migrated correctly - that is, all mails are in the correct Folders. So I wrote a Powershell Script performing the following functions:
     - Go through the Folders of the correct Mailbox
     - Look up each mail in the "All documents" Folder of the "wrong" Mailbox by comparing the subject
     - If the mail is found, get the correct Folder in the "wrong" Mailbox (the Folders exist but they are empty...)
     - Move the mail to the Folder found in the previous step

    All this is done by connecting to the Exchange Server via EWS API.
    The script works fine, walking through all Folders, identifying all mails... But the mail.move Operation Fails :(

    I have to admit that I am not the well-versed Powershell / EWS programmer, so there's a big Chance that I built in some minor or Major Bugs... I have marked the suspicous lines in Routine recurse_Through_folders ()....

    I would be grateful for any tipps and/or corrections...

    Best regards,
    Andreas

    -------------------------------------
    #################
    # Compares the folders in 2 databases.
    # Moves the items in database 1 to the folders they hold in database2
    #################

    function Connect-Exchange{
        param(
              [Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
                [Parameter(Position=1, Mandatory=$true)] [System.Management.Automation.PSCredential]$Credentials
        )  
           Begin
                 {
                Load-EWSManagedAPI

                ## Set Exchange Version  
                $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1

                ## Create Exchange Service Object  
                $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)  

                ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials  

                #Credentials Option 1 using UPN for the windows Account  
                #$psCred = Get-Credential  
                $creds = New-Object System.Net.NetworkCredential($Credentials.UserName.ToString(),$Credentials.GetNetworkCredential().password.ToString())  
                $service.Credentials = $creds      
                #Credentials Option 2  
                #service.UseDefaultCredentials = $true  
                 #$service.TraceEnabled = $true
                ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates  

                Handle-SSL      

                ## Set the URL of the CAS (Client Access Server) to use two options are availbe to use Autodiscover to find the CAS URL or Hardcode the CAS to use  

                #CAS URL Option 1 Autodiscover  
                #$service.AutodiscoverUrl($MailboxName,{$true})  

                #CAS URL Option 2 Hardcoded  

                $uri=[system.URI] "https://mail-cas2.stadtwerke-erfurt.net/ews/exchange.asmx"  
                $service.Url = $uri    

                Write-host ("Using CAS Server : " + $Service.url)  

                ## Optional section for Exchange Impersonation  

                #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)
                if(!$service.URL){
                      throw "Error connecting to EWS"
                }
                else
                {            
                      return $service
                }
          }
    }

    function Load-EWSManagedAPI{
        param(
        )  
           Begin
          {
                ## Load Managed API dll  
                ###CHECK FOR EWS MANAGED API, IF PRESENT IMPORT THE HIGHEST VERSION EWS DLL, ELSE EXIT
                $EWSDLL = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services'|Sort-Object Name -Descending| Select-Object -First 1 -ExpandProperty Name)).'Install Directory') + "Microsoft.Exchange.WebServices.dll")
                if (Test-Path $EWSDLL)
                    {
                    Import-Module $EWSDLL
                    }
                else
                    {
                    "$(get-date -format yyyyMMddHHmmss):"
                    "This script requires the EWS Managed API 1.2 or later."
                    "Please download and install the current version of the EWS Managed API from"
                    "http://go.microsoft.com/fwlink/?LinkId=255472"
                    ""
                    "Exiting Script."
                    exit
                    }
            }
    }

    function Handle-SSL{
        param(
        )  
           Begin
          {
                ## Code From http://poshcode.org/624
                ## Create a compilation environment
                $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider
                $Compiler=$Provider.CreateCompiler()
                $Params=New-Object System.CodeDom.Compiler.CompilerParameters
                $Params.GenerateExecutable=$False
                $Params.GenerateInMemory=$True
                $Params.IncludeDebugInformation=$False
                $Params.ReferencedAssemblies.Add("System.DLL")| Out-Null

    $TASource=@'
      namespace Local.ToolkitExtensions.Net.CertificatePolicy{
        public class TrustAll : System.Net.ICertificatePolicy {
          public TrustAll() {
          }
          public bool CheckValidationResult(System.Net.ServicePoint sp,
            System.Security.Cryptography.X509Certificates.X509Certificate cert,
            System.Net.WebRequest req, int problem) {
            return true;
          }
        }
      }
    '@
                $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)
                $TAAssembly=$TAResults.CompiledAssembly

                ## We now create an instance of the TrustAll and attach it to the ServicePointManager
                $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")
                [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll

                ## end code from http://poshcode.org/624

          }
    }

    function Get-FolderFromPath{
          param (
                  [Parameter(Position=0, Mandatory=$true)] [string]$FolderPath,
                      [Parameter(Position=1, Mandatory=$true)] [string]$MailboxName,
                      [Parameter(Position=2, Mandatory=$true)] [Microsoft.Exchange.WebServices.Data.ExchangeService]$service,
                      [Parameter(Position=3, Mandatory=$false)] [Microsoft.Exchange.WebServices.Data.PropertySet]$PropertySet
                  )
          process{
                ## Find and Bind to Folder based on Path  
                #Define the path to search should be seperated with \  
                #Bind to the MSGFolder Root  
                $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)  
                $tfTargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)  
                #Split the Search path into an array  
                $fldArray = $FolderPath.Split("\")
                 #Loop through the Split Array and do a Search for each level of folder
                for ($lint = 1; $lint -lt $fldArray.Length; $lint++) {
                  #Perform search based on the displayname of each folder level
                  $fvFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1)
                      if(![string]::IsNullOrEmpty($PropertySet)){
                            $fvFolderView.PropertySet = $PropertySet
                      }
                  $SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,$fldArray[$lint])
                  $findFolderResults = $service.FindFolders($tfTargetFolder.Id,$SfSearchFilter,$fvFolderView)
                  if ($findFolderResults.TotalCount -gt 0){
                      foreach($folder in $findFolderResults.Folders){
                          $tfTargetFolder = $folder                
                      }
                  }
                  else{
                      Write-host ("Error Folder Not Found: $fldArray[$lint] - check path and try again")  
                      $tfTargetFolder = $null  
                      break  
                  }    
              }  
                if($tfTargetFolder -ne $null){
                      return [Microsoft.Exchange.WebServices.Data.Folder]$tfTargetFolder
                }
                else{
                      #throw ("Folder Not found")
                return $null
                }
          }
    }



    function Get-FolderItems
    {
        [CmdletBinding()]
        param(
            [Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
            [Parameter(Position=1, Mandatory=$true)] [System.Management.Automation.PSCredential]$Credentials,
            [Parameter(Position=2, Mandatory=$true)] [string]$FolderPath
        )  
        Begin
            {

            $service = Connect-Exchange -MailboxName $MailboxName -Credentials $Credentials
            $folder = Get-FolderFromPath -FolderPath $FolderPath  -Mailboxname $MailboxName -service $service
            if ([string]::IsNullOrEmpty($folder)) {return $null}
            $SubFolderId =  new-object Microsoft.Exchange.WebServices.Data.FolderId($folder.id.UniqueID)

            #Define ItemView to retrieve just 10000 Items    
            $ivItemView =  New-Object Microsoft.Exchange.WebServices.Data.ItemView(10000)
            $PR_RETENTION_DATE = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301C,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::SystemTime);
            $ItemPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
            $ItemPropset.Add($PR_RETENTION_DATE);
            $ivItemView.PropertySet = $ItemPropset
            $rptCollection = @{}
            $fiItems = $null    
            $fiItems = $service.FindItems($SubFolderId,$ivItemView)    
            if (![string]::IsNullOrEmpty($fiItems)){[Void]$service.LoadPropertiesForItems($fiItems,$ItemPropset)}
            return $fiItems
            }
    }
    # End GetFolderItems


    function RecurseThroughFolders
    {
        [CmdletBinding()]
        param (
              [Parameter(Position=0, Mandatory=$true)] [string]$Mailbox,
              [Parameter(Position=1, Mandatory=$true)] [string]$MailboxToBeSorted,
            [Parameter(Position=2, Mandatory=$true)] [System.Management.Automation.PSCredential]$Credentials,
              [Parameter(Position=3, Mandatory=$true)] $ItemListToBeMoved
        )

        # Liste der Ordner, die NICHT durchsucht werden sollen
        $exclusions = @("/Kontakte/Recipient Cache",
                        "/(Alle)",
                        "/(Mail-Verlauf)",
                        "/(Vorlage)",
                        "/Sync Issues",
                        "/Sync Issues/Conflicts",
                        "/Sync Issues/Local Failures",
                        "/Sync Issues/Server Failures",
                        "/Recoverable Items",
                        "/Deletions",
                        "/Purges",
                        "/Versions",
                            "/Synchronisierungsprobleme",
                            "/Synchronisierungsprobleme/Konflikte",
                            "/Synchronisierungsprobleme/Lokale Fehler",
                            "/Synchronisierungsprobleme/Serverfehler",
                            "/Gelöschte Elemente",
                        "/Gesendete Elemente",
                        "/Entwürfe",
                        "/Aufgaben",
                        "/Journal",
                        "/Junk-E-Mail",
                        "/Kalender",
                        "/Notizen",
                        "/Postausgang"
                        "/Posteingang"
                        )

        # Liste aller Ordner aus der richtig sortierten Mailbox lesen (ohne obige Exclusions)
        $mailboxfolders = @(Get-MailboxFolderStatistics $Mailbox | Where {!($exclusions -icontains $_.FolderPath)} | Select FolderPath,FolderID)
        # EWS mit der Mailbox verbinden, die sortiert werden soll
        $serviceToSort = Connect-Exchange -MailboxName $MailboxToBeSorted -Credentials $Credentials

        # Loop über alle gefundenen Ordner
        foreach ($mailboxfolder in $mailboxfolders)
        {
            $folder = $mailboxfolder.FolderPath.Replace("/","\")
            if (($folder -match "Top of Information Store") -or ($folder -match "Oberste Ebene des Informationsspeichers"))
            {
                # Die oberste Ebene wird übersprungen, da es erst in den Unterordnern losgeht...
                $folder = $folder.Replace("\Top of Information Store","\")
                $folder = $folder.Replace("\Oberste Ebene des Informationsspeichers","\")
            }
            else
            {
                Write-Host "Processing folder $folder" -ForeGroundColor Green

            # Hier wird die Liste der Mails aus dem aktuellen Ordner der richtig sortierten Mailbox gelesen
                $ItemListToCheck = Get-FolderItems -MailboxName $Mailbox -Credentials $Credentials -FolderPath $folder
                if ([string]::IsNullOrEmpty($ItemListToCheck))
                {
                    Write-Host "Folder is empty: $folder" -ForegroundColor Red
                }
                else
                {
                # Die Liste der richtig sortierten Mails durchgehen...
                    foreach ($itmCorrect in $ItemListToCheck)
                    {
                    # Jede Mail in der Liste der falsch sortierten Mails suchen...
                        foreach ($itmToBeMoved in $ItemListToBeMoved)
                        {
                        # Wenn die Mail gefunden wurde...
                            #Write-Host "Correct: $itmCorrect.Subject"
                            #Write-Host "Move   : $itmToBeMoved.Subject"
                            #$SubjectCorrect = [string] $itmCorrect.Subject.Replace('*','')
                            #$SubjectToMove  = [string] $itmtoBeMoved.Subject.Replace('*','')
                            $SubjectCorrect = [string] $itmCorrect.Subject
                            $SubjectToMove  = [string] $itmtoBeMoved.Subject
                            if ($SubjectCorrect -eq $SubjectToMove)
                            {
                            # ...die Mail aus der Originalmailbox in den richtigen Ordner sortieren
                                      try
                                      {
                                # Zielordner in der zu sortierenden Mailbox suchen
                                    $targetFolderID = Get-FolderFromPath -FolderPath $folder -MailboxName $MailboxToBeSorted -service $serviceToSort
                                # ....und einzusortierende Mail in den Ordner verschieben
                                            if ($targetFolderID -ne $null)
                                    {
                                      ########### HERE SEEMS TO BE SOME ERROR, SINCE WE ALWAYS GET THE MESSAGE
                                     ########### "Failed to move item...."
                                        $SubFolderId =  new-object Microsoft.Exchange.WebServices.Data.FolderId($targetfolderID.id.UniqueID)
                                       $retVal = [Microsoft.Exchange.WebServices.Data.EmailMessage]$ItmToBeMoved.Move($SubFolderId);
                                    }
                                    else
                                    {
                                        Write-Host "Targetfolder $folder not found in destination." -ForegroundColor Red
                                    }
                                      }
                                      catch
                                      {
                                            Write-Host "Failed to move item $SubjectToMove to folder $folder"  -foregroundcolor Red
                                      }

                            }
                        } # foreach ($itmCorrect...
                    } # foreach ($itmToBeMoved...
                } # endif (isNullOrEmpty (ItemListToCheck)...
            } #endif (($folder -match "Top of Information Store")...
        } # foreach ($mailboxfolder in $mailboxfolders)
    }
    # End RecurseThroughFolder

    #########################################
    # Main
    # Username und Passwort holen - sollte als "mailservice" laufen
    $Credential = Get-Credential
    # Unter dem Usernamen eine Exchange Session aufmachen
    try
    {
        $MSXSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://mail-db2.stadtwerke-erfurt.net/Powershell/ -Authentication Kerberos -Credential $Credential
        Import-PSSession $MSXSession -AllowClobber
    }
    catch
    {
        Write-Host "Fehlerhafte Anmeldung!" -ForegroundColor Cyan
        Exit
    }

    #########################################
    # HIER DIE MAILBOXADRESSEN ANPASSEN, WENN ANDERE DATENBANKEN SORTIERT WERDEN SOLLEN:
    # SMTP Address of the mailbox, which has to be sorted
    $MailboxToSort = "wrong_mailbox@contoso.com"
    $FolderToSort = '\Notes-Ordner\All Documents'

    # SMTP Address of correct Mailbox
    $MailboxCorrect = "correct_mailbox@contoso.com"
    #########################################

    Write-Host "Getting list of items to be moved from folder $FolderToSort in mailbox $MailboxToSort" -foregroundcolor Green
    # Liste der fehlerhaft sortierten Mails einlesen aus der Original-Mailbox
    $ItemListToBeMoved =$null
    $ItemListToBeMoved = Get-FolderItems -MailboxName $MailboxToSort -Credentials $Credential -FolderPath $FolderToSort
    if ([string]::IsNullOrEmpty($ItemListToBeMoved))
    {
        Write-Host "No mails found to be sorted in folder $FolderToSort in mailbox $MailboxToSort - exiting" -ForegroundColor Red
    }
    else
    {
        # Durch alle Ordner der richtig sortierten Mailbox gehen, items einlesen...
        Write-Host "Looping through all folders of correct mailbox $MailboxCorrect" -foregroundcolor Green
        RecurseThroughFolders -Mailbox $MailboxCorrect -MailboxToBeSorted $MailboxToSort -Credentials $Credential $ItemListToBeMoved
    }

                                                                                                                                                                      

    Senior Consultant

    Wednesday, October 12, 2016 1:00 PM

All replies

  • >>  $retVal = [Microsoft.Exchange.WebServices.Data.EmailMessage]$ItmToBeMoved.Move($SubFolderId);

    Move will return null when you move a message between two mailboxes (even if its successful) it will only return an Item if an item is moved within a Mailbox between two folders. (Have you actually checked the target mailbox folder to see if the copy worked). You might want to enable tracing to see what the actual response is from the server.

    Cheers
    Glen

    Thursday, October 13, 2016 2:34 AM
  • Since $ItmToBeMoved points to a mail message in $MailBoxToBeSorted and $SubFolderID is retrieved from the same Mailbox, it always should be a move within one single Mailbox...

    Anyway, I always get an error :(


    Senior Consultant

    Friday, October 14, 2016 1:28 PM
  • But what is the error ? you need to enable tracing and look at the error message that is being returned from the server when you attempt to move which will then help tell you what the solution might be. eg all you need is

    $service.TraceEnabled = $true

    before the line of code that fails and you should then see what the server is telling you is wrong

    Sunday, October 16, 2016 10:55 PM