Quantcast
Channel: Glen's Exchange and Office 365 Dev Blog
Viewing all 241 articles
Browse latest View live

Sending a message with voting buttons using EWS

$
0
0
Voting button's in emails are one the long standing features of using Outlook with Exchange, in Exchange 2013 SP1 there is now support for responding to Vote requests with EWS . However there is still no direct support for sending a message with voting buttons in EWS, so in this post I'm going to show a method you can use to send a message with Voting Button in EWS by setting the PidLidVerbStream extended Property on a message which is documented in the http://msdn.microsoft.com/en-us/library/ee218541(v=exchg.80).aspx .

I used this property a few months back in another script for stopping ReplyAll on a message, this script uses much the same code with the addition of Hex values for the Voting options to include in the Message. Firstly each voting option you want to add uses the VoteOption Structure http://msdn.microsoft.com/en-us/library/ee218406(v=exchg.80).aspx . This is reasonable straight forward eg

$ApproveOption = "0400000007417070726F76650849504D2E4E6F74650007417070726F766500000000000000000001000000020000000200000001000000FFFFFFFF"

equates to

 04000000 VerbType
 07 Length of the DisplayName eg the length of (Approve)
 417070726F7665 (Approve in Hex)
  08 Length of the MsgClsNameCount  eg the length of (IPM.Note)
 49504D2E4E6F7465 (IPM.Note in Hex)
 00 Internal1StringCount
 07417070726F7665 Repeat of the DisplayName and Length

The rest of the options are listed in the documentation link the one interesting option is SendBehavior which allows you to control if you want the user to be prompted about the response of to just send it immediately. In my example scripts its set to prompt the user

The following script will send a message using EWS with Appove/Reject voting buttons  I've put a download of this script here the code itself looks like

  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
$MailboxName = $args[0]
$SentTo = $args[1]

$VerbSetting = "" | Select DisableReplyAll,DisableReply,DisableForward,DisableReplyToFolder
$VerbSetting.DisableReplyAll = $false
$VerbSetting.DisableReply = $false
$VerbSetting.DisableForward = $false
$VerbSetting.DisableReplyToFolder = $false

## 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
}

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

## 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($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())
$service.Credentials = $creds

#Credentials Option 2
#service.UseDefaultCredentials = $true

## Choose to ignore any SSL Warning issues caused by Self Signed Certificates

## 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

## 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})
"Using CAS Server : " + $Service.url

#CAS URL Option 2 Hardcoded

#$uri=[system.URI] "https://casservername/ews/exchange.asmx"
#$service.Url = $uri

## Optional section for Exchange Impersonation

#$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)


functionGet-VerbStream{
param (
$VerbSettings = "$( throw 'VerbSettings is a mandatory Parameter' )"
)
process{

$Header = "02010600000000000000"
$ReplyToAllHeader = "055265706C790849504D2E4E6F7465074D657373616765025245050000000000000000"
$ReplyToAllFooter = "0000000000000002000000660000000200000001000000"
$ReplyToHeader = "0C5265706C7920746F20416C6C0849504D2E4E6F7465074D657373616765025245050000000000000000"
$ReplyToFooter = "0000000000000002000000670000000300000002000000"
$ForwardHeader = "07466F72776172640849504D2E4E6F7465074D657373616765024657050000000000000000"
$ForwardFooter = "0000000000000002000000680000000400000003000000"
$ReplyToFolderHeader = "0F5265706C7920746F20466F6C6465720849504D2E506F737404506F737400050000000000000000"
$ReplyToFolderFooter = "00000000000000020000006C00000008000000"
$ApproveOption = "0400000007417070726F76650849504D2E4E6F74650007417070726F766500000000000000000001000000020000000200000001000000FFFFFFFF"
$RejectOption= "040000000652656A6563740849504D2E4E6F7465000652656A65637400000000000000000001000000020000000200000002000000FFFFFFFF"
$VoteOptionExtras = "0401055200650070006C00790002520045000C5200650070006C007900200074006F00200041006C006C0002520045000746006F007200770061007200640002460057000F5200650070006C007900200074006F00200046006F006C00640065007200000741007000700072006F00760065000741007000700072006F007600650006520065006A0065006300740006520065006A00650063007400"
if($VerbSetting.DisableReplyAll){
$DisableReplyAllVal = "00"
}
else{
$DisableReplyAllVal = "01"
}
if($VerbSetting.DisableReply){
$DisableReplyVal = "00"
}
else{
$DisableReplyVal = "01"
}
if($VerbSetting.DisableForward){
$DisableForwardVal = "00"
}
else{
$DisableForwardVal = "01"
}
if($VerbSetting.DisableReplyToFolder){
$DisableReplyToFolderVal = "00"
}
else{
$DisableReplyToFolderVal = "01"
}
$VerbValue = $Header + $ReplyToAllHeader + $DisableReplyAllVal + $ReplyToAllFooter + $ReplyToHeader + $DisableReplyVal +$ReplyToFooter + $ForwardHeader + $DisableForwardVal + $ForwardFooter + $ReplyToFolderHeader + $DisableReplyToFolderVal + $ReplyToFolderFooter + $ApproveOption + $RejectOption + $VoteOptionExtras
return$VerbValue
}
}

function hex2binarray($hexString){
$i = 0
[byte[]]$binarray = @()
while($i-le$hexString.length - 2){
$strHexBit = ($hexString.substring($i,2))
$binarray += [byte]([Convert]::ToInt32($strHexBit,16))
$i = $i + 2
}
return ,$binarray
}



$VerbStreamProp = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,0x8520, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)

$VerbSettingValue = get-VerbStream$VerbSetting

$EmailMessage = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage -ArgumentList $service
$EmailMessage.Subject = "Message Subject"
#Add Recipients
$EmailMessage.ToRecipients.Add($SentTo)
$EmailMessage.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody
$EmailMessage.Body.BodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::HTML
$EmailMessage.Body.Text = "Body"
$EmailMessage.SetExtendedProperty($VerbStreamProp,(hex2binarray $VerbSettingValue))
$EmailMessage.SendAndSaveCopy()

Finding a Contact with a Body flag using EWS and eDiscovery (eg added from Microsoft Lync)

$
0
0
With the average size of Mailbox\Archives getting larger by the day eDiscovery on Exchange 2013 is useful for a broad array of tasks. eDiscovery makes use of the KQL (Keyword Query Language https://msdn.microsoft.com/EN-US/library/office/ee558911(v=office.15).aspx ) which allows you to query for both free text and Queryable properties that have been indexed by the Exchange store and it also allows the use of some more complex operators such as proximity and Synonyms.

Let's look at a specific user case for eDiscover, the Lync client on 2013 will automatically add contacts to your Exchange Mailbox in the "Lync Contacts" folder and flag the body of the contact with something like

2/02/2015 This contact was added from Microsoft Lync 2013 (15.0.4675.1000)

(if you want to bind directly to the Lync Contacts folder in 2013 you should be able to use the QuickContacts WellKnownFolder Enum eg

$folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::QuickContacts,$MailboxName)  
$LyncContacts = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)

)

If you wanted to look at a Mailbox and find all the contacts that where added by the Lync client regardless of what folder that are now located in then eDiscovery is good option. For this you would need to construct a KQL query that first restricted the results to Contacts only using

Kind:contacts

Then add another predicate to this query using a Boolean And to perform a freetext query for the phrase

added from Microsoft Lync

The reason for using a freetext query rather then a property query eg Body:SearchPhrase is explained in the Notes section of https://technet.microsoft.com/en-us/library/jj983804%28v=exchg.150%29.aspx . Put simply the Body property is searchable (available in Freetext) but not Queryable (meaning you can do a property restriction).

To use eDiscovery is EWS you need to granted the Discovery Search RBAC role https://technet.microsoft.com/en-us/library/dd298059%28v=exchg.150%29.aspx

The following script does a eDiscovery of the Mailbox you enter as a commandline parameter for the KQL Kind:contacts And "added from Microsoft Lync". It then does a batch GetItem on the results and validate the Body of the contact to ensure its not False positive and produce a csv report like

 
I've put a download of this script here the script itself looks like
 
 


  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
## Get the Mailbox to Access from the 1st commandline argument

$MailboxName = $args[0]

$KQL = "Kind:contacts And `"added from Microsoft Lync`"";
$SearchableMailboxString = $MailboxName;

## 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
}

## Set Exchange Version
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_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($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())
$service.Credentials = $creds
#$service.TraceEnabled = $true
#Credentials Option 2
#service.UseDefaultCredentials = $true

## Choose to ignore any SSL Warning issues caused by Self Signed Certificates

## 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

## 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})
"Using CAS Server : " + $Service.url

#CAS URL Option 2 Hardcoded

#$uri=[system.URI] "https://casservername/ews/exchange.asmx"
#$service.Url = $uri

## Optional section for Exchange Impersonation

#$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)
function ConvertToString($ipInputString){
$Val1Text = ""
for ($clInt=0;$clInt-lt$ipInputString.length;$clInt++){
$Val1Text = $Val1Text + [Convert]::ToString([Convert]::ToChar([Convert]::ToInt32($ipInputString.Substring($clInt,2),16)))
$clInt++
}
return$Val1Text
}


function GetFolderPaths{
param (
$rootFolderId = "$( throw 'rootFolderId is a mandatory Parameter' )",
$Archive = "$( throw 'Archive is a mandatory Parameter' )"
)
process{
#Define Extended properties
$PR_FOLDER_TYPE = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(13825,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);
$folderidcnt = $rootFolderId
#Define the FolderView used for Export should not be any larger then 1000 folders due to throttling
$fvFolderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)
#Deep Transval will ensure all folders in the search path are returned
$fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep;
$psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$PR_Folder_Path = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(26293, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);
#Add Properties to the Property Set
$psPropertySet.Add($PR_Folder_Path);
$fvFolderView.PropertySet = $psPropertySet;
#The Search filter will exclude any Search Folders
$sfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($PR_FOLDER_TYPE,"1")
$fiResult = $null
#The Do loop will handle any paging that is required if there are more the 1000 folders in a mailbox
do {
$fiResult = $Service.FindFolders($folderidcnt,$sfSearchFilter,$fvFolderView)
foreach($ffFolderin$fiResult.Folders){
$foldpathval = $null
#Try to get the FolderPath Value and then covert it to a usable String
if ($ffFolder.TryGetProperty($PR_Folder_Path,[ref]$foldpathval))
{
$binarry = [Text.Encoding]::UTF8.GetBytes($foldpathval)
$hexArr = $binarry | ForEach-Object { $_.ToString("X2") }
$hexString = $hexArr -join ''
$hexString = $hexString.Replace("FEFF", "5C00")
$fpath = ConvertToString($hexString)
}
"FolderPath : " + $fpath
if($Archive){
$Script:FolderCache.Add($ffFolder.Id.UniqueId,"\Archive-Mailbox\" + $fpath);
}
else{
$Script:FolderCache.Add($ffFolder.Id.UniqueId,$fpath);
}
}
$fvFolderView.Offset += $fiResult.Folders.Count
}while($fiResult.MoreAvailable -eq$true)
}
}

$Script:FolderCache = New-Object system.collections.hashtable
GetFolderPaths -rootFolderId (new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)) -Archive $false
#GetFolderPaths -rootFolderId (new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::ArchiveMsgFolderRoot,$MailboxName)) -Archive $true

$gsMBResponse = $service.GetSearchableMailboxes($SearchableMailboxString, $false);
$msbScope = New-Object Microsoft.Exchange.WebServices.Data.MailboxSearchScope[] $gsMBResponse.SearchableMailboxes.Length
$mbCount = 0;
foreach ($sbMailboxin$gsMBResponse.SearchableMailboxes)
{
$msbScope[$mbCount] = New-Object Microsoft.Exchange.WebServices.Data.MailboxSearchScope($sbMailbox.ReferenceId, [Microsoft.Exchange.WebServices.Data.MailboxSearchLocation]::All);
$mbCount++;
}
$smSearchMailbox = New-Object Microsoft.Exchange.WebServices.Data.SearchMailboxesParameters
$mbq = New-Object Microsoft.Exchange.WebServices.Data.MailboxQuery($KQL, $msbScope);
$mbqa = New-Object Microsoft.Exchange.WebServices.Data.MailboxQuery[] 1
$mbqa[0] = $mbq
$smSearchMailbox.SearchQueries = $mbqa;
$smSearchMailbox.PageSize = 100;
$smSearchMailbox.PageDirection = [Microsoft.Exchange.WebServices.Data.SearchPageDirection]::Next;
$smSearchMailbox.PerformDeduplication = $false;
$smSearchMailbox.ResultType = [Microsoft.Exchange.WebServices.Data.SearchResultType]::PreviewOnly;
$srCol = $service.SearchMailboxes($smSearchMailbox);
$rptCollection = @()

if ($srCol[0].Result -eq[Microsoft.Exchange.WebServices.Data.ServiceResult]::Success)
{
Write-Host ("Items Found " + $srCol[0].SearchResult.ItemCount)
if ($srCol[0].SearchResult.ItemCount -gt 0)
{
do
{
$smSearchMailbox.PageItemReference = $srCol[0].SearchResult.PreviewItems[$srCol[0].SearchResult.PreviewItems.Length - 1].SortValue;
$type = ("System.Collections.Generic.List"+'`'+"1") -as"Type"
$type = $type.MakeGenericType("Microsoft.Exchange.WebServices.Data.ItemId"-as"Type")
$BatchItemids = [Activator]::CreateInstance($type)
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
foreach ($PvItemin$srCol[0].SearchResult.PreviewItems) {
$BatchItemids.Add($PvItem.Id)
}
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$Results = $service.BindToItems($BatchItemids,$psPropset)
foreach($Resultin$Results){
if($Result.Item.Body.Text -ne$null){
if($Result.Item.Body.Text.Contains("This contact was added from Microsoft Lync")){
$rptObj = "" | select FolderPath,Subject,ImAddress1,ImAddress2,ImAddress3
$rptObj.ImAddress1 = $Result.Item.ImAddresses[[Microsoft.Exchange.WebServices.Data.ImAddressKey]::ImAddress1]
$rptObj.ImAddress2 = $Result.Item.ImAddresses[[Microsoft.Exchange.WebServices.Data.ImAddressKey]::ImAddress2]
$rptObj.ImAddress3 = $Result.Item.ImAddresses[[Microsoft.Exchange.WebServices.Data.ImAddressKey]::ImAddress3]
$rptObj.Subject = $Result.Item.Subject
if($Script:FolderCache.ContainsKey($Result.Item.ParentFolderId.UniqueId)){
$rptObj.FolderPath = $Script:FolderCache[$Result.Item.ParentFolderId.UniqueId]
}
$rptCollection += $rptObj
}
}
}

$srCol = $service.SearchMailboxes($smSearchMailbox);
Write-Host("Items Remaining : " + $srCol[0].SearchResult.ItemCount);
} while ($srCol[0].SearchResult.ItemCount-gt 0 );

}

}
$rptCollection
$rptCollection | Export-Csv -NoTypeInformation -Path c:\temp\AddedByLync.csv


 

Tracking and reporting on the Clutter Folder Item statistics using EWS in Exchange Online

$
0
0
If you haven't missed it Clutter is a new feature in Exchange Online that is trying to help deal with the deluge of email that most people receive each day using machine learning. Clutter has been live for a little while now and processing away in the background (if you have enabled it) so I decided to put together a script that will report on what Clutter has been up to using EWS. So the following script produces a report that is emailed to the owner of the Mailbox the script is run against. The report itself looks likes the following (I've been trying out some new HTML code that's optimized for mobile devices)



So the script first enumerates all the Items in the clutter folder and then does some aggregation to work out how many messages in the clutter folder where received in the last 7 days, pervious week and month. It also calculates the total number of attachments ,senders and senders that are in the GAL. To work out if a Sender is in the Global Address List the script does a resolve on each of the senders. The result or each resolution is cached so the same sender address is only checked once.

I've put a download of the script here the code itself looks like

  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
## Get the Mailbox to Access from the 1st commandline argument

$MailboxName = $args[0]

## 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
}

## Set Exchange Version
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_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($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())
$service.Credentials = $creds
#$service.TraceEnabled = $true
#Credentials Option 2
#service.UseDefaultCredentials = $true

## Choose to ignore any SSL Warning issues caused by Self Signed Certificates

## 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

## 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})
"Using CAS Server : " + $Service.url

#CAS URL Option 2 Hardcoded

#$uri=[system.URI] "https://casservername/ews/exchange.asmx"
#$service.Url = $uri

## Optional section for Exchange Impersonation

#$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)

function ConvertId{
param (
$HexId = "$( throw 'HexId is a mandatory Parameter' )"
)
process{
$aiItem = New-Object Microsoft.Exchange.WebServices.Data.AlternateId
$aiItem.Mailbox = $MailboxName
$aiItem.UniqueId = $HexId
$aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::HexEntryId
$convertedId = $service.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId)
return$convertedId.UniqueId
}
}

$ReportObj = "" | Select FirstMessage,LastMessage,TotalCount,TotalSize,Last7Count,Last7Size,Last7Direction,PreviousWeekCount,PreviousWeekSize,Last30Count,Last30Size,Senders,GalSenders,NumberOfAttachments,Attachments,SendersInGal,SendersNotInGal
$ReportObj.Senders = @{}
$ReportObj.GalSenders = @{}
$ReportObj.Attachments = @{}
$ReportObj.NumberOfAttachments = 0
$ReportObj.SendersInGal = 0
$ReportObj.SendersNotInGal = 0

# Bind to the Root Folder
$folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,$MailboxName)
$ClutterFolderEntryId = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([System.Guid]::Parse("{23239608-685D-4732-9C55-4C95CB4E8E33}"), "ClutterFolderEntryId", [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$psPropset.Add($ClutterFolderEntryId)
$RootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid,$psPropset)
$FolderIdVal = $null
if ($RootFolder.TryGetProperty($ClutterFolderEntryId,[ref]$FolderIdVal))
{
$Clutterfolderid= new-object Microsoft.Exchange.WebServices.Data.FolderId((ConvertId -HexId ([System.BitConverter]::ToString($FolderIdVal).Replace("-",""))))
$ClutterFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$Clutterfolderid);
#Define ItemView to retrive just 1000 Items
$ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)
$fiItems = $null
do{
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$fiItems = $service.FindItems($ClutterFolder.Id,$ivItemView)
#$fiItems = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$ivItemView)
if($fiItems.Items.Count -gt 0){
[Void]$service.LoadPropertiesForItems($fiItems,$psPropset)
foreach($Itemin$fiItems.Items){
$ReportObj.TotalCount++
if($ReportObj.TotalCount -eq 1){
$ReportObj.LastMessage = "From : " + $Item.Sender.Name + " Subject : " + $Item.Subject
}
$ReportObj.FirstMessage = "From : " + $Item.Sender.Name + " Subject : " + $Item.Subject
$ReportObj.TotalSize += $Item.Size
##DateStuff
if($Item.DateTimeReceived -gt (Get-Date).AddDays(-7)){
$ReportObj.Last7Count++
$ReportObj.Last7Size+= $Item.Size
}
if($Item.DateTimeReceived -gt (Get-Date).AddDays(-14) -band$Item.DateTimeReceived -le (Get-Date).AddDays(-7)){
$ReportObj.PreviousWeekCount++
$ReportObj.PreviousWeekSize+= $Item.Size
}
if($Item.DateTimeReceived -gt (Get-Date).AddMonths(-1)){
$ReportObj.Last30Count++
$ReportObj.Last30Size+= $Item.Size
}
if($Item.Attachments.Count -gt 0){
$ReportObj.NumberOfAttachments += $Item.Attachments.Count
foreach($Attachemntin$Item.Attachments){
if(!$Attachemnt.IsInline){
if($Attachemnt.Name -ne$null){
if($Attachemnt.Name.Length -gt 2){
if(!$ReportObj.Attachments.Contains($Attachemnt.Name)){
$ReportObj.Attachments.Add($Attachemnt.Name,1)
}
else{
$ReportObj.Attachments[$Attachemnt.Name]++
}
}
}
}

}
}
if($Item.Sender -ne$null){
if($Item.Sender.Address -ne$null){
if(!$ReportObj.Senders.Contains($Item.Sender.Address)){
$ReportObj.Senders.Add($Item.Sender.Address,1)
$ncCol = $service.ResolveName($Item.Sender.Address, [Microsoft.Exchange.WebServices.Data.ResolveNameSearchLocation]::DirectoryOnly, $false);
if($ncCol.Count -gt 0){
$ReportObj.GalSenders.Add($Item.Sender.Address,1)
$ReportObj.SendersInGal++
}
else{
$ReportObj.SendersNotInGal++
}
}
else{
$ReportObj.Senders[$Item.Sender.Address] += 1
if($ReportObj.GalSenders.Contains($Item.Sender.Address)){
$ReportObj.GalSenders[$Item.Sender.Address] += 1
}
}
}
}
}
}
$ivItemView.Offset += $fiItems.Items.Count
}while($fiItems.MoreAvailable -eq$true)
}
else{
"Clutter Folder not found"
}
if($ReportObj.Last7Count -gt$ReportObj.PreviousWeekCount){
$ReportObj.Last7Direction = "Assending"
}
else{
$ReportObj.Last7Direction = "Descending"
}
$Reporthtml = @"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="application/xhtml+xml; charset=utf-8" />
<meta content="width=device-width" name="viewport" />
<title>Clutter Statistics</title>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
<meta content="telephone=no" name="format-detection" />
<style type="text/css"> /* RESET */ #outlook a {padding:0;} body {width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; mso-line-height-rule:exactly;} table td { border-collapse: collapse; } .ExternalClass {width:100%;} .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;} table td {border-collapse: collapse;} /* IMG */ img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;} a img {border:none;} /* Becoming responsive */ @media only screen and (max-device-width: 480px) { table[id="container_div"] {max-width: 480px !important;} table[id="container_table"], table[class="image_container"], table[class="image-group-contenitor"] {width: 100% !important; min-width: 320px !important;} table[class="image-group-contenitor"] td, table[class="mixed"] td, td[class="mix_image"], td[class="mix_text"], td[class="table-separator"], td[class="section_block"] {display: block !important;width:100% !important;} table[class="image_container"] img, td[class="mix_image"] img, table[class="image-group-contenitor"] img {width: 100% !important;} table[class="image_container"] img[class="natural-width"], td[class="mix_image"] img[class="natural-width"], table[class="image-group-contenitor"] img[class="natural-width"] {width: auto !important;} a[class="button-link justify"] {display: block !important;width:auto !important;} td[class="table-separator"] br {display: none;} td[class="cloned_td"] table[class="image_container"] {width: 100% !important; min-width: 0 !important;} } table[class="social_wrapp"] {width: auto;} </style>
</head>
<body style=" background-color: #d5e4ed;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" bgcolor="#d5e4ed"

align="center" style="text-align:center; background-color:#d5e4ed; border-collapse: collapse"

id="container_div">
<tbody>
<tr>
<td align="center"><br />
<table cellspacing="0" cellpadding="0" border="0" id="container_wrapper">
<tbody>
<tr>
<td>
<table width="600" cellspacing="0" cellpadding="0" border="0"

bgcolor="#ffffff" style="border-collapse: collapse; min-width: 600px;"

id="container_table">
<tbody>
<tr>
<td valign="top" bgcolor="#ffffff">
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#007fff" style="border-collapse: collapse; background-color: rgb(0, 127, 255);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(255, 255, 255);">
<div style="text-align: center; color: rgb(255, 255, 255);"><span

style="color: rgb(255, 255, 255); line-height: 130%;"><span

style="font-size: 28px; line-height: 130%;"><span

style="font-family: tahoma, geneva, sans-serif; line-height: 130%;"><strong

style="color: rgb(255, 255, 255);">Clutter
Statistics</strong></span></span></span></div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td valign="top" bgcolor="#ffffff">
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#00a8cc" style="border-collapse: collapse; background-color: rgb(0, 168, 204);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(0, 0, 0);">
<table cellspacing="0" cellpadding="5" border="0"

align="center" class="cke_show_border">
<tbody>
<tr>
<td colspan="2" rowspan="1" style="width: 600px; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><u><strong><span

style="font-size: 26px; line-height: 130%;">Folder
Totals</span></strong></u></td>
</tr>
<tr>
<td style="width: 20%; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#1#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);">
<div><span style="font-size: 18px; line-height: 130%;"><strong>Total
Item Count</strong></span></div>
</td>
</tr>
<tr>
<td style="text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#2#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Total
Item Size (MB)</strong></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td valign="top" bgcolor="#ffffff">
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#00a8cc" style="border-collapse: collapse; background-color: rgb(0, 168, 204);">
<tbody>
<tr valign="top">
<td valign="top" height="2" align="left">
<table width="100%" cellspacing="0" cellpadding="0"

border="0" align="center" style="border-top-width: 1px; border-top-style: solid; border-top-color: rgb(0, 0, 0); margin: 0px auto;"

class="divider">
<tbody>
<tr>
<td style="line-height: 0; text-decoration: none; padding: 0px;">
  </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#6fcee2" style="border-collapse: collapse; background-color: rgb(111, 206, 226);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(0, 0, 0);">
<table cellspacing="0" cellpadding="5" border="0"

align="center" class="cke_show_border">
<tbody>
<tr>
<td colspan="2" rowspan="1" style="width: 600px; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><strong><u><span

style="font-size: 26px; line-height: 130%;">Last
7 Days</span></u></strong></td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#3#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Total
Item Count</strong></span></td>
</tr>
<tr>
<td style="text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#4#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Tota
Item Size (MB)</strong></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#00d2ff" style="border-collapse: collapse; background-color: rgb(0, 210, 255);">
<tbody>
<tr valign="top">
<td valign="top" height="2" align="left">
<table width="100%" cellspacing="0" cellpadding="0"

border="0" align="center" style="border-top-width: 1px; border-top-style: solid; border-top-color: rgb(0, 0, 0); margin: 0px auto; color: rgb(0, 0, 0);"

class="divider">
<tbody>
<tr>
<td style="line-height: 0; text-decoration: none; padding: 0px;">
  </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#00d2ff" style="border-collapse: collapse; background-color: rgb(0, 210, 255);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 14px; font-family: Arial, Helvetica, sans-serif; color: rgb(102, 102, 102); line-height: 130%;"></span>
<table cellspacing="0" cellpadding="5" border="0"

align="center" class="cke_show_border">
<tbody>
<tr>
<td colspan="2" rowspan="1" style="width: 600px; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><u><strong><span

style="font-size: 26px; line-height: 130%;">7-14
Days</span></strong></u></td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#5#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Total
Item Count</strong></span></td>
</tr>
<tr>
<td style="text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#6#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Tota
Item Size (MB)</strong></span></td>
</tr>
</tbody>
</table>
<span style="font-size: 14px; font-family: Arial, Helvetica, sans-serif; color: rgb(102, 102, 102); line-height: 130%;">
</span></td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#00d2ff" style="border-collapse: collapse; background-color: rgb(0, 210, 255);">
<tbody>
<tr valign="top">
<td valign="top" height="2" align="left">
<table width="100%" cellspacing="0" cellpadding="0"

border="0" align="center" style="border-top-width: 1px; border-top-style: solid; border-top-color: #333; margin: 0 auto;"

class="divider">
<tbody>
<tr>
<td style="line-height: 0; text-decoration: none; padding: 0px;">
  </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#66e4ff" style="border-collapse: collapse; background-color: rgb(102, 228, 255);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(0, 0, 0);">
<table cellspacing="0" cellpadding="5" border="0"

align="center" class="cke_show_border">
<tbody>
<tr>
<td colspan="2" rowspan="1" style="width: 600px; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><u><strong><span

style="font-size: 26px; line-height: 130%;">Last
30 Days</span></strong></u></td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#7#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Total
Item Count</strong></span></td>
</tr>
<tr>
<td style="text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#8#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Tota
Item Size (MB)</strong></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#66e4ff" style="border-collapse: collapse; background-color: rgb(102, 228, 255);">
<tbody>
<tr valign="top">
<td valign="top" height="2" align="left">
<table width="100%" cellspacing="0" cellpadding="0"

border="0" align="center" style="border-top-width: 1px; border-top-style: solid; border-top-color: #333; margin: 0 auto;"

class="divider">
<tbody>
<tr>
<td style="line-height: 0; text-decoration: none; padding: 0px;">
  </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#93ecff" style="border-collapse: collapse; background-color: rgb(147, 236, 255);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(0, 0, 0);">
<table cellspacing="0" cellpadding="5" border="0"

align="center" class="cke_show_border">
<tbody>
<tr>
<td colspan="2" rowspan="1" style="width: 600px; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><u><strong><span

style="font-size: 26px; line-height: 130%;">Folder
Statistics</span></strong></u></td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><br />
</td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#9#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Total
Number of Attachments</strong></span></td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#10#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Total
Number of Senders</strong></span></td>
</tr>
<tr>
<td style="width: 20%; text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#11#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Senders
in Gal</strong></span></td>
</tr>
<tr>
<td style="text-align: right; text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 20px; line-height: 130%;">#12#</span></td>
<td style="text-decoration: none; padding: 5px; line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 18px; line-height: 130%;"><strong>Senders
not in Gal</strong></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#93ecff" style="border-collapse: collapse; background-color: rgb(147, 236, 255);">
<tbody>
<tr valign="top">
<td valign="top" height="2" align="left">
<table width="100%" cellspacing="0" cellpadding="0"

border="0" align="center" style="border-top-width: 1px; border-top-style: solid; border-top-color: #333; margin: 0 auto;"

class="divider">
<tbody>
<tr>
<td style="line-height: 0; text-decoration: none; padding: 0px;">
  </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#b2e5e5" style="border-collapse: collapse; background-color: rgb(178, 229, 229);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 22px; line-height: 130%;"><b><u><span

style="font-size: 26px; line-height: 130%;">Top
Senders</span></u><br />
<br />
</b><span style="font-size: 18px; line-height: 130%;">#13#</span></span></td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#b2e5e5" style="border-collapse: collapse; background-color: rgb(178, 229, 229);">
<tbody>
<tr valign="top">
<td valign="top" height="2" align="left">
<table width="100%" cellspacing="0" cellpadding="0"

border="0" align="center" style="border-top-width: 1px; border-top-style: solid; border-top-color: #333; margin: 0 auto;"

class="divider">
<tbody>
<tr>
<td style="line-height: 0; text-decoration: none; padding: 0px;">
  </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#a0cece" style="border-collapse: collapse; background-color: rgb(160, 206, 206);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(0, 0, 0);"><strong><u><span

style="font-size: 26px; line-height: 130%;">Top
Gal Senders</span></u></strong><br />
<br />
<span style="font-size: 18px; line-height: 130%;">#14#</span></td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#a0cece" style="border-collapse: collapse; background-color: rgb(160, 206, 206);">
<tbody>
<tr valign="top">
<td valign="top" height="2" align="left">
<table width="100%" cellspacing="0" cellpadding="0"

border="0" align="center" style="border-top-width: 1px; border-top-style: solid; border-top-color: #333; margin: 0 auto;"

class="divider">
<tbody>
<tr>
<td style="line-height: 0; text-decoration: none; padding: 0px;">
  </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table width="100%" cellspacing="0" cellpadding="10"

border="0" bgcolor="#86becb" style="border-collapse: collapse; background-color: rgb(134, 190, 203);">
<tbody>
<tr valign="top">
<td valign="top" style="line-height: 130%; color: rgb(0, 0, 0);"><span

style="font-size: 26px; line-height: 130%;"><strong></strong><u><strong>Top
Attachment Names​</strong><br />
</u><span style="font-size: 18px; line-height: 130%;">#15#</span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>

"@
$ReportObj
$Reporthtml = $Reporthtml.replace("#1#",$ReportObj.TotalCount)
$Reporthtml = $Reporthtml.replace("#2#",[Math]::Round(($ReportObj.TotalSize/1024/1024),2))
$Reporthtml = $Reporthtml.replace("#3#",$ReportObj.Last7Count)
$Reporthtml = $Reporthtml.replace("#4#",[Math]::Round(($ReportObj.Last7Size/1024/1024),2))
$Reporthtml = $Reporthtml.replace("#5#",$ReportObj.PreviousWeekCount)
$Reporthtml = $Reporthtml.replace("#6#",[Math]::Round(($ReportObj.PreviousWeekSize/1024/1024),2))
$Reporthtml = $Reporthtml.replace("#7#",$ReportObj.Last30Count)
$Reporthtml = $Reporthtml.replace("#8#",[Math]::Round(($ReportObj.Last30Size/1024/1024),2))
$Reporthtml = $Reporthtml.replace("#9#",$ReportObj.NumberOfAttachments)
$Reporthtml = $Reporthtml.replace("#10#",$ReportObj.Senders.Count)
$Reporthtml = $Reporthtml.replace("#11#",$ReportObj.SendersInGal)
$Reporthtml = $Reporthtml.replace("#12#",$ReportObj.SendersNotInGal)
$tc = 0
$ReportObj.Senders.GetEnumerator() | Sort-Object Value -descending | ForEach-Object {
$tc++
if($tc-le 10){
$SendersList = $SendersList + $_.Name + " : " + $_.Value + "<br>"
}
}
$tc = 0
$ReportObj.GalSenders.GetEnumerator() | Sort-Object Value -descending | ForEach-Object {
$tc++
if($tc-le 10){
$GalSendersList = $GalSendersList + $_.Name + " : " + $_.Value + "<br>"
}
}
$tc = 0
$ReportObj.Attachments.GetEnumerator() | Sort-Object Value -descending | ForEach-Object {
$tc++
if($tc-le 10){
$AttachmentsList = $AttachmentsList + $_.Name + " : " + $_.Value + "<br>"
}
}
$Reporthtml = $Reporthtml.replace("#13#",$SendersList)
$Reporthtml = $Reporthtml.replace("#14#",$GalSendersList)
$Reporthtml = $Reporthtml.replace("#15#",$AttachmentsList)

$toAddress = $MailboxName
$EmailMessage = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage -ArgumentList $service
$EmailMessage.Subject = "Clutter Statistics"
#Add Recipients
$EmailMessage.ToRecipients.Add($toAddress)
$EmailMessage.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody
$EmailMessage.Body.BodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::HTML
$EmailMessage.Body.Text = $Reporthtml
$EmailMessage.Send()


Export calendar Items to a CSV file using EWS and Powershell

$
0
0
Somebody asked about this last week and while I have a lot of EWS scripts that do access the Calendar I didn't have a simple example that just exported a list of the Calendar events with relevant information to a CSV file so here it is.

I've talked on this one before in this howto but when you query the calendar folder using EWS you need to use a CalendarView which will expand any recurring appointments in a calendar. There are some limits when you use a calendarview in that you can only return a maximum of 2 years of appointments at a time and paging will limit the max number of items to 1000 per call. So if you have a calendar with a very large number of appointments you need to break your query into small date time blocks. In this example script I'm just grabbing the next 7 days of appointments if you want to query a longer period you need to adjust the following lines (keeping in mind what I just mentioned)

#Define Date to Query
$StartDate = (Get-Date)
$EndDate = (Get-Date).AddDays(7) 

The AppointmentState value being reporting on is this property I've put some code in that converts the Bitwise value back to its Enumeration Flags value (or values) so it makes it more readable.

With the Attendees I've also include the acceptance status if this is stored, note the acceptance status of all the attendees and resources will only be available on the Organizer copy of an appointment (which will be stored in the Organizers calendar).  The script produces a CSV that looks like



I've put a download of the script here the code itself looks like

  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
## Get the Mailbox to Access from the 1st commandline argument

$MailboxName = $args[0]

## 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
}

## 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($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())
$service.Credentials = $creds
#Credentials Option 2
#service.UseDefaultCredentials = $true

## Choose to ignore any SSL Warning issues caused by Self Signed Certificates

## 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

## 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})
"Using CAS Server : " + $Service.url

#CAS URL Option 2 Hardcoded

#$uri=[system.URI] "https://casservername/ews/exchange.asmx"
#$service.Url = $uri

## Optional section for Exchange Impersonation

#$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)

# Bind to the Calendar Folder
$folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar,$MailboxName)
$Calendar = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)
$Recurring = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Appointment, 0x8223,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Boolean);
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$psPropset.Add($Recurring)
$psPropset.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text;

$AppointmentState = @{0 = "None"; 1 = "Meeting"; 2 = "Received";4 = "Canceled"; }

#Define Date to Query
$StartDate = (Get-Date)
$EndDate = (Get-Date).AddDays(7)

$RptCollection = @()


#Define the calendar view
$CalendarView = New-Object Microsoft.Exchange.WebServices.Data.CalendarView($StartDate,$EndDate,1000)
$fiItems = $service.FindAppointments($Calendar.Id,$CalendarView)
if($fiItems.Items.Count -gt 0){
[Void]$service.LoadPropertiesForItems($fiItems,$psPropset)
}
foreach($Itemin$fiItems.Items){
$rptObj = "" | Select StartTime,EndTime,Duration,Type,Subject,Location,Organizer,Attendees,Resources,AppointmentState,Notes,HasAttachments,IsReminderSet,ReminderDueBy
$rptObj.StartTime = $Item.Start
$rptObj.EndTime = $Item.End
$rptObj.Duration = $Item.Duration
$rptObj.Subject = $Item.Subject
$rptObj.Type = $Item.AppointmentType
$rptObj.Location = $Item.Location
$rptObj.Organizer = $Item.Organizer.Address
$rptObj.HasAttachments = $Item.HasAttachments
$rptObj.IsReminderSet = $Item.IsReminderSet
$rptObj.ReminderDueBy = $Item.ReminderDueBy
$aptStat = "";
$AppointmentState.Keys | where { $_-band$Item.AppointmentState } | foreach { $aptStat += $AppointmentState.Get_Item($_) + ""}
$rptObj.AppointmentState = $aptStat
$RptCollection += $rptObj
foreach($attendeein$Item.RequiredAttendees){
$atn = $attendee.Address + " Required "
if($attendee.ResponseType -ne$null){
$atn += $attendee.ResponseType.ToString() + "; "
}
else{
$atn += "; "
}
$rptObj.Attendees += $atn
}
foreach($attendeein$Item.OptionalAttendees){
$atn = $attendee.Address + " Optional "
if($attendee.ResponseType -ne$null){
$atn += $attendee.ResponseType.ToString() + "; "
}
else{
$atn += "; "
}
$rptObj.Attendees += $atn
}
foreach($attendeein$Item.Resources){
$atn = $attendee.Address + " Resource "
if($attendee.ResponseType -ne$null){
$atn += $attendee.ResponseType.ToString() + "; "
}
else{
$atn += "; "
}
$rptObj.Resources += $atn
}
$rptObj.Notes = $Item.Body.Text
"Start : " + $Item.Start
"Subject : " + $Item.Subject

}
$RptCollection | Export-Csv -NoTypeInformation -Path "c:\temp\$MailboxName-CalendarCSV.csv"

Downloading a shared file from Onedrive for business using Powershell

$
0
0
I thought I'd quickly share this script I came up with to download a file that was shared using One Drive for Business (which is SharePoint under the covers) with Powershell. The following script takes a OneDrive for business URL which would look like

https://mydom-my.sharepoint.com/personal/gscales_domain_com/Documents/Email%20attachments/filename.txt

This script is pretty simple it uses the SharePoint CSOM (Client side object Model) which it loads in the first line. It uses the URI object to separate the host and relative URL which the CSOM requires and also the SharePointOnlineCredentials object to handle the Office365 SharePoint online authentication.

The following script is a function that take the OneDrive URL, Credentials for Office365 and path you want to download the file to and downloads the file. eg to run the script you would use something like

./spdownload.ps1 'https://mydom-my.sharepoint.com/personal/gscales_domain_com/Documents/Email%20attachments/filename.txt'

I've put a download of the script here the script itself looks like

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$SharePointClientDll = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SharePoint Client Components\'|Sort-Object Name -Descending| Select-Object -First 1 -ExpandProperty Name)).'Location') + "ISAPI\Microsoft.SharePoint.Client.dll")
Add-Type -Path $SharePointClientDll


function DownloadFileFromOneDrive{
param (
$DownloadURL = "$( throw 'DownloadURL is a mandatory Parameter' )",
$PSCredentials = "$( throw 'credentials is a mandatory Parameter' )",
$DownloadPath = "$( throw 'DownloadPath is a mandatory Parameter' )"
)
process{
$DownloadURI = New-Object System.Uri($DownloadURL);
$SharepointHost = "https://" + $DownloadURI.Host
$soCredentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($PSCredentials.UserName.ToString(),$PSCredentials.password)
$clientContext = New-Object Microsoft.SharePoint.Client.ClientContext($SharepointHost)
$clientContext.Credentials = $soCredentials;
$destFile = $DownloadPath + [System.IO.Path]::GetFileName($DownloadURI.LocalPath)
$fileInfo = [Microsoft.SharePoint.Client.File]::OpenBinaryDirect($clientContext, $DownloadURI.LocalPath);
$fstream = New-Object System.IO.FileStream($destFile, [System.IO.FileMode]::Create);
$fileInfo.Stream.CopyTo($fstream)
$fstream.Flush()
$fstream.Close()
Write-Host ("File downloaded to " + ($destFile))
}
}

$cred = Get-Credential
#exmample use
DownloadFileFromOneDrive -DownloadURL $args[0] -PSCredentials $cred -DownloadPath 'c:\Temp\'

Configure the default calendar view in OWA in Exchange 2013 and Exchange Online using EWS and Powershell

$
0
0
If you are after a higher level of control over the default setting in OWA, EWS offers you the ability to configure many of the default setting by modifying the FAI (folder associated Item) configuration item that controls that setting. One example of a setting that you can modify is the default view of the calendar in OWA  eg  the following setting


When are user makes a change to this setting in OWA, the setting is stored in an FAI item with an Item Class of OWA.ViewStateConfiguration in a roaming dictionary property called CalendarViewTypeDesktop.

The values for each of the setting are

1 = Day
2 = Week
3 = Work week
4 = Month

The following script first checks to see if the OWA.ViewStateConfiguration config item exists (on a new mailbox if the user has never logged into OWA it probably wont exist).

To use this script you run it with the name of the Mailbox you want to run it against and the value for the enum you want to use eg to set the default as Work week use

./SetOWACalendarView.ps1 mailbox@domain.com 3

I've put a download of this script here

The code itself looks like

  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
## Get the Mailbox to Access from the 1st commandline argument
$MailboxName = $args[0]

$CalendarSetting = $args[1]

## 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
}

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

## 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($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())
$service.Credentials = $creds
#$service.TraceEnabled = $true
#Credentials Option 2
#service.UseDefaultCredentials = $true

## Choose to ignore any SSL Warning issues caused by Self Signed Certificates

## 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

## 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})
"Using CAS Server : " + $Service.url

#CAS URL Option 2 Hardcoded

#$uri=[system.URI] "https://casservername/ews/exchange.asmx"
#$service.Url = $uri

## Optional section for Exchange Impersonation

#$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)

#Check for existing Item
$folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,$MailboxName)
$SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass,"IPM.Configuration.OWA.ViewStateConfiguration")
$ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1)
$ivItemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Associated
$fiResults = $service.FindItems($folderid,$SfSearchFilter,$ivItemView)
if($fiResults.Items.Count -eq 0){
Write-Host ("No Config Item found, create new Item")
$UMConfig = New-Object Microsoft.Exchange.WebServices.Data.UserConfiguration -ArgumentList $service
$UMConfig.Save("OWA.ViewStateConfiguration",$folderid)
}
else{
Write-Host ("Existing Config Item Found");
}
#Specify the Root folder where the FAI Item is
$UsrConfig = [Microsoft.Exchange.WebServices.Data.UserConfiguration]::Bind($service, "OWA.ViewStateConfiguration", $folderid, [Microsoft.Exchange.WebServices.Data.UserConfigurationProperties]::All)
if($UsrConfig.Dictionary.ContainsKey("CalendarViewTypeDesktop")){
$UsrConfig.Dictionary["CalendarViewTypeDesktop"] = $CalendarSetting
}
else{
$UsrConfig.Dictionary.Add("CalendarViewTypeDesktop",$CalendarSetting)
}
$UsrConfig.Update()
"Mailbox Updated"

Clutter Threshold and Probability Outlook form

$
0
0
Clutter is one of the new features in Exchange Online in Office365, while there is some really good info on how to use clutter the detailed info on how it works behind the scenes is a little harder to come by. There was a really good blog post recently http://blogs.technet.com/b/inside_microsoft_research/archive/2015/04/09/probabilistic-programming-goes-large-scale-from-reducing-email-clutter-to-any-machine-learning-task.aspx from one the Microsoft Research team that gives some good information on how it actually works using Infer.NET and Probabilistic Programming.

With new features in Exchange come new MAPI properties and clutter in no different so we have these two new properties that get set on items



Back in 2004 when SCL (SPAM confidence level) was introduced in Exchange 2003 Paul Bowden gave us this CFG trick to expose the SCL value in Outlook http://blogs.technet.com/b/exchange/archive/2004/05/26/142607.aspx . All these years later the same trick can be used to expose these Clutter properties in Outlook so when you do get a message in Outlook that you think should have been clutter you can see how close it was to being classified. eg in action


The details on the CFG file is we define 2 properties to show


[Properties]
Property01=ClutterProbability
Property02=ClutterThreshold

These properties are PT_Double so the Mapi property type is x0005 which is why the type=5, and both properties are Named Properties

[Property.ClutterProbability]
Type=5
NmidPropSet = {23239608-685D-4732-9C55-4C95CB4E8E33}
NmidString = ClutterProbability
DisplayName = Clutter Probability
[Property.ClutterThreshold]
Type=5
NmidPropSet = {23239608-685D-4732-9C55-4C95CB4E8E33}
NmidString = ClutterThreshold
DisplayName = Clutter Threshold

To use this CFG file you need to copy it into the same directory where the

IPML.ico
IPMS.ico

files are located on Outlook 2013 that will be in

C:\Program Files\Microsoft Office 15\root\office15\FORMS\1033

(or Program Files (x86) if you using the 32 bit version)

On 2010 it will be Microsoft Office 14

If you can't find the files just do a search of the hard disk for them (Sorry I don't have the rights to redistribute them).

Once you have the form in that directory you go through the same install procedure as http://blogs.technet.com/b/exchange/archive/2004/05/26/142607.aspx just replace SCL with Clutter Extension Form. SlipStick.com has also created a video on doing a CFG install https://www.youtube.com/watch?v=FfW6JknE_IQ which is quite good.

I've put a download of the file here the full file looks like

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
[Description]
MessageClass=IPM.Note
CLSID={00020D31-0000-0000-C000-000000000046}
DisplayName=Clutter Extension Form
Category=Standard
Subcategory=Form
Comment=This form is used to expose the Clutter settings
LargeIcon=IPML.ico
SmallIcon=IPMS.ico
Version=1.0
VersionMajor=1
VersionMinor=0
Locale=enu
Hidden=1
Owner=Clutter
ComposeInFolder=1

[Platforms]
Platform2=NTx86
Platform9=Win95

[Platform.NTx86]
CPU=ix86
OSVersion=WinNT3.5

[Platform.Win95]
CPU=ix86
OSVersion=Win95

[Properties]
Property01=ClutterProbability
Property02=ClutterThreshold

[Property.ClutterProbability]
Type=5
NmidPropSet = {23239608-685D-4732-9C55-4C95CB4E8E33}
NmidString = ClutterProbability
DisplayName = Clutter Probability

[Property.ClutterThreshold]
Type=5
NmidPropSet = {23239608-685D-4732-9C55-4C95CB4E8E33}
NmidString = ClutterThreshold
DisplayName = Clutter Threshold

[Verbs]
Verb1=1

[Verb.1]
DisplayName=&Open
Code=0
Flags=0
Attribs=2

[Extensions]
Extensions1=1

[Extension.1]
Type=30
NmidPropset={00020D0C-0000-0000-C000-000000000046}
NmidInteger=1
Value=1000000000000000

Working with Referance Attachments in EWS on Exchange Online

$
0
0
Last year a new feature was introduced into Exchange Online to allow you to create an Attachment where the Attachment resided on OneDrive rather then the traditional Email attachment approach you can read more about this here http://blogs.office.com/2014/10/08/introducing-new-way-share-files-outlook-web-app/ there was also an Ignite session you can watch on how this has evolved https://channel9.msdn.com/Events/Ignite/2015/BRK2196. MAPI has always had a rich set of attachment types since Exchange's inception to deal with Rich Text Messaging eg the various attachment types you can encounter are listed in https://msdn.microsoft.com/en-us/library/office/cc815439.aspx  . In EWS these attachment types where simplified some what into either Item Attachments (for Attached Store Items) or File Attachments. To cater for this new feature a new Attachment Type called a
ReferenceAttachment has been introduced into EWS in Exchange Online. To see the
ReferenceAttachmentType you need to have your requested Server version set to Exchange2003_SP1 else the attachment will just be returned as a FileAttachment .  Essential this is what you will see in a EWS Response when you have a reference attachment

 1
2
3
4
5
6
7
8
9
10
11
12
13
<t:ReferenceAttachment>
<t:AttachmentIdId="AAMkADczNDE4YWE...."/>
<t:Name>lb.txt</t:Name>
<t:ContentType>image/png</t:ContentType>
<t:ContentId>7cc92386-bee2-4db8-ae81-d17dad9350f8</t:ContentId>
<t:Size>598</t:Size>
<t:LastModifiedTime>2015-05-08T06:39:22</t:LastModifiedTime>
<t:IsInline>false</t:IsInline>
<t:AttachLongPathName>https://domain-my.sharepoint.com/personal/gscales_datarumble_com/Documents/Email%20attachments/lb.txt</t:AttachLongPathName>
<t:ProviderType>OneDrivePro</t:ProviderType>
<t:PermissionType>2</t:PermissionType>
<t:AttachmentIsFolder>false</t:AttachmentIsFolder>
</t:ReferenceAttachment>

If your using Proxy code and you've updated your WSDL proxies you should be able to use the  ReferenceAttachmentType to access the AttachLongPathName property etc. If your using the EWS Managed API there currently is no support for Reference Attachments in the last released version or the Open Source Repo https://github.com/OfficeDev/ews-managed-api . So I've forked the open source repo and added support in for this (note its read only support) into my GitHub repo https://github.com/gscales/ews-managed-api . For those that just want the binaries https://github.com/gscales/EWS-Binaries/blob/master/RAFork-Microsoft.Exchange.WebServices.zip Note this is patched build not a stable build so use it at your own risk.

To use this in code with the CSOM to download the Attachment from One drive looks like

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
            FindItemsResults<Item> fiResults = service.FindItems(WellKnownFolderName.Inbox, new ItemView(1));
Item Message = fiResults.Items[0];
Message.Load();
foreach (Attachment Attachment in Message.Attachments)
{
if (Attachment is ReferenceAttachment)
{
ReferenceAttachment rfAttach = (ReferenceAttachment)Attachment;
Console.WriteLine(rfAttach.ProviderType);
Console.WriteLine(rfAttach.AttachLongPathName);

var credentials = new Microsoft.SharePoint.Client.SharePointOnlineCredentials(username, securedPassword);
Uri url = new Uri(rfAttach.AttachLongPathName);
Microsoft.SharePoint.Client.ClientContext clientContext = new Microsoft.SharePoint.Client.ClientContext("https://" + url.Host);
clientContext.Credentials = credentials;
Microsoft.SharePoint.Client.FileInformation fileInfo = Microsoft.SharePoint.Client.File.OpenBinaryDirect(clientContext, url.LocalPath);
FileStream fs = new FileStream("c:\\temp\\" + rfAttach.Name, FileMode.Create);
fileInfo.Stream.CopyTo(fs);
fs.Flush();
fs.Close();

}

}
Or in a Script that will download the last email in the Mailbox and download any reference attachments from that message I've put a download of this script here

  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
####################### 
<#
.SYNOPSIS
Download any OneDrive Attachments on the last message received in a Mailbox in Exchange Online

.DESCRIPTION
Using the EWS Managed API and the Sharepoint CSOM this scripts downloads the any OneDrive Attachments
on the last message received in a Mailbox in Exchange Online

Requires Forked version of the EWS Managed API dll from
https://github.com/gscales/EWS-Binaries/blob/master/RAFork-Microsoft.Exchange.WebServices.zip
Requires Sharepoint CSOM

.EXAMPLE
PS C:\>Download-LastOneDriveAttachment -MailboxName user.name@domain.com -FilePath c:\temp

This example downloads any OneDrive Attachments on the last message received in a Mailbox to the c:\temp directory
#>
function Download-LastOneDriveAttachment
{
[CmdletBinding()]
param(
[Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
[Parameter(Position=0, Mandatory=$true)] [string]$FilePath
)
Begin
{
$SharePointClientDll = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SharePoint Client Components\'|Sort-Object Name -Descending| Select-Object -First 1 -ExpandProperty Name)).'Location') + "ISAPI\Microsoft.SharePoint.Client.dll")
Add-Type -Path $SharePointClientDll

## Load Managed API dll note forked version to support referance attachments
$EWSDLL = "c:\temp\Microsoft.Exchange.WebServices.dll"
Import-Module$EWSDLL

## Set Exchange Version
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_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($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())
$soCredentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($psCred.UserName.ToString(),$psCred.password)
$service.Credentials = $creds
#Credentials Option 2
#service.UseDefaultCredentials = $true

## Choose to ignore any SSL Warning issues caused by Self Signed Certificates

## 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

## 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})
"Using CAS Server : " + $Service.url

#CAS URL Option 2 Hardcoded

#$uri=[system.URI] "https://casservername/ews/exchange.asmx"
#$service.Url = $uri

## Optional section for Exchange Impersonation
}
Process
{
# Bind to the Inbox Folder
$folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)
$Inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)
#Define ItemView to retrive just 1000 Items
$ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1)
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$fiItems = $service.FindItems($Inbox.Id,$ivItemView)
[Void]$service.LoadPropertiesForItems($fiItems,$psPropset)
foreach($Itemin$fiItems.Items){
Write-Host ("Processing : " + $Item.Subject)
foreach($Attachmentin$Item.Attachments){
if($Attachment-is[Microsoft.Exchange.WebServices.Data.ReferenceAttachment]){
$DownloadURI = New-Object System.Uri($Attachment.AttachLongPathName);
$SharepointHost = "https://" + $DownloadURI.Host
$clientContext = New-Object Microsoft.SharePoint.Client.ClientContext($SharepointHost)
$clientContext.Credentials = $soCredentials;
$destFile = $FilePath + "\" + [System.IO.Path]::GetFileName($DownloadURI.LocalPath)
$fileInfo = [Microsoft.SharePoint.Client.File]::OpenBinaryDirect($clientContext, $DownloadURI.LocalPath);
$fstream = New-Object System.IO.FileStream($destFile, [System.IO.FileMode]::Create);
$fileInfo.Stream.CopyTo($fstream)
$fstream.Flush()
$fstream.Close()
Write-Host ("File downloaded to " + ($destFile))
}
}
}
}
End{}
}




Using Office 365 Groups within the EWS Managed API

$
0
0
Office365 Groups (or unified\modern groups) is a new feature the was introduced in Office365 that Exchange Online is an active participant in by providing the Email/Conversation and calendar portions of this feature. Recently there was a new unified REST API released in preview https://msdn.microsoft.com/en-us/office/office365/howto/office-365-unified-api-overview that I'll look at another day but with this post I want to look at just accessing the Exchange portion of the unified Groups using EWS and also we will take a look at how the config is stored in the mailbox.

Accessing using the EWS Managed API

If your following the changes to the EWS Managed API open source repository https://github.com/OfficeDev/ews-managed-api support for reading the available Unified groups a Mailbox is a member of was added in April. This is done using the yet to be documented GetUserUnifiedGroups EWS operation. So if you have a version of the Managed API compiled from this source that includes these updates (there is a sample one from one of my other post here ) . You can then use the following code to get all the Unified Groups a Mailbox is a member of

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
            RequestedUnifiedGroupsSet Group = new RequestedUnifiedGroupsSet();
Group.FilterType = UnifiedGroupsFilterType.All;
Group.SortDirection = SortDirection.Ascending;
Group.SortType = UnifiedGroupsSortType.DisplayName;
List<RequestedUnifiedGroupsSet> reqG = new List<RequestedUnifiedGroupsSet>();
reqG.Add(Group);
Collection<UnifiedGroupsSet> ugGroupSet = service.GetUserUnifiedGroups(reqG,"jcool@domain.com");
foreach (UnifiedGroupsSet ugset in ugGroupSet)
{
foreach (UnifiedGroup ugGroup in ugset.Groups)
{
Console.WriteLine(ugGroup.SMTPAddress);
}
}
 
or in PowerShell

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
$Group = New-Object Microsoft.Exchange.WebServices.Data.Groups.RequestedUnifiedGroupsSet
$Group.FilterType = [Microsoft.Exchange.WebServices.Data.Groups.UnifiedGroupsFilterType]::All;
$Group.SortDirection = [Microsoft.Exchange.WebServices.Data.SortDirection]::Ascending;
$Group.SortType = [Microsoft.Exchange.WebServices.Data.Groups.UnifiedGroupsSortType]::DisplayName;
$reqG = New-Object -TypeName 'System.Collections.Generic.List[Microsoft.Exchange.WebServices.Data.Groups.RequestedUnifiedGroupsSet]'
$reqG.Add($Group);
$ugGroupSet = $service.GetUserUnifiedGroups($reqG,"jcool@domain.com");
foreach ($ugsetin$ugGroupSet)
{
foreach ($gGroupin$ugset.Groups)
{
$gGroup.SmtpAddress
}
}

(Note as I'm writing this I think there is a bug in this operation when using the Mailbox overload)

Once you have the SMTPAddress of the Group you can just Bind to the Group using a normal Folder bind eg either the Inbox (for conversations) or the Calendar

1
2
$folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar,$gGroup.SmtpAddress)   
$Calendar = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)

Technical Bit

For those people interested in the how this get implemented in your mailbox here's what it looks like. In the Mailbox Non_IPM_Subtree there is a folder called MailboxAssociations this gets created eg



Then in the normal Messages collection of this folder when you subscribe to a group (or are made a member of one) an item of type IPM.MailboxAssociation.Group will be created. On this Item's extended properties get set with the relevant information for the group eg




So if you have an older version of the Managed API where you don't have access to the newer updates you can still get the Group memberships for a mailbox by accessing these IPM.MailboxAssociation.Group objects and then reading the extended properties. Eg first to bind to the MailboxAssociations use the following extended property which will give you the HexId of the folder which you can then convert to an EWSId to bind to the folder.

1
2
3
4
5
6
7
8
            ExtendedPropertyDefinition MailboxAssociationFolderEntryId = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.Common, "MailboxAssociationFolderEntryId", MapiPropertyType.Binary);
PropertySet fldPropSet = new PropertySet(BasePropertySet.FirstClassProperties);
fldPropSet.Add(MailboxAssociationFolderEntryId);
Folder rtFolder = Folder.Bind(service, new FolderId(WellKnownFolderName.Root,"jcool@datarumble.com"), fldPropSet);
Byte[] MAFldId = null;
rtFolder.TryGetProperty(MailboxAssociationFolderEntryId, out MAFldId);
AlternateId aiEWS = (AlternateId)service.ConvertId(new AlternateId(IdFormat.HexEntryId, BitConverter.ToString(MAFldId).Replace("-", ""), "jcool@datarumble.com"), IdFormat.EwsId);
Folder MbAssoc = Folder.Bind(service,new FolderId(aiEWS.UniqueId));

Once you bind to the MailboxAssoications Folder you can then just use a FindItem op with a search filter to limit the results to just the Group associations. Then pull the extended property for the LegacyDN of the MailboxAssoication then you just need to resolve that to the SMTPAddress eg

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
            PropertySet itmPropSet = new PropertySet(BasePropertySet.FirstClassProperties);
ExtendedPropertyDefinition MailboxAssociationExternalId = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.Common, "MailboxAssociationExternalId", MapiPropertyType.String);
ExtendedPropertyDefinition MailboxAssociationLegacyDN = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.Common, "MailboxAssociationLegacyDN", MapiPropertyType.String);
ExtendedPropertyDefinition MailboxAssociationIsMember = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.Common, "MailboxAssociationIsMember", MapiPropertyType.Boolean);
itmPropSet.Add(MailboxAssociationExternalId);
itmPropSet.Add(MailboxAssociationLegacyDN);
itmPropSet.Add(MailboxAssociationIsMember);
SearchFilter sfFilter = new SearchFilter.IsEqualTo(ItemSchema.ItemClass, "IPM.MailboxAssociation.Group");
ItemView ivMbAsView = new ItemView(1000);
ivMbAsView.PropertySet = itmPropSet;
FindItemsResults<Item> GroupAssoications = MbAssoc.FindItems(sfFilter, ivMbAsView);
List<String> GroupSMTPAddresses = new List<string>();
foreach (Item itItem in GroupAssoications.Items)
{
String mbAssoicatedDn = "";
bool MailboxAssociationIsMemberValue = false;
itItem.TryGetProperty(MailboxAssociationIsMember, out MailboxAssociationIsMemberValue);
if (MailboxAssociationIsMemberValue)
{
if (itItem.TryGetProperty(MailboxAssociationLegacyDN, out mbAssoicatedDn))
{
NameResolutionCollection ncCol = service.ResolveName(mbAssoicatedDn, ResolveNameSearchLocation.DirectoryOnly, true);
if (ncCol.Count > 0)
{
GroupSMTPAddresses.Add(ncCol[0].Mailbox.Address);
}
}
}
}

So as you can see it's a little more complicated this way so try to use the other op where you can.

Quick Mailbox Item Type eDiscovery Powershell script

$
0
0
One of the features of eDiscovery in Exchange 2013 and greater is the ability to do a search of Mailbox and just return the statistics on particular Items (or searches). This allows you to do a really quick search without the need to process any of the ResultSet of the Search.  To limit the Items types returned by an eDiscovery query you can use the kind Keyword property in a KQL query. The feature set of which has recently been updated https://technet.microsoft.com/en-us/library/dn508399(v=exchg.150).aspx to now allow you to include Lync (or IM) items. So one little cool thing you can do with this using a multiple OR logic query with eDiscovery is get a quick list of the number of exchange Items by type in a Mailbox eg the KQL for this would look like

"kind:email OR kind:meetings OR kind:contacts OR kind:tasks OR kind:notes OR kind:IM OR kind:rssfeeds OR kind:voicemail"

and this would produce a report that looks like



I've put together a sample of a script to do this which you can download form here. To run the script use

Get-MailboxItemTypeStats -MailboxName user@domain.com -OutputFileName reportfile.csv

The code itself looks like

  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
####################### 
<#
.SYNOPSIS
Performs an eDiscovery Search using Powershell and the Exchange Web Services API in a Mailbox and produces a CSV report of ItemType in that Mailbox

.DESCRIPTION
Performs an eDiscovery Search using Powershell and the Exchange Web Services API in a Mailbox and produces a CSV report of ItemType in that Mailbox

Requires the EWS Managed API from https://www.microsoft.com/en-us/download/details.aspx?id=42951

.EXAMPLE
PS C:\>Get-MailboxItemTypeStats -MailboxName user.name@domain.com -OutputFileName Report.csv

This Example Performs an eDiscovery Search using Powershell and the Exchange Web Services API in a Mailbox and produces a CSV report of ItemType in that Mailbox
#>
functionGet-MailboxItemTypeStats
{
[CmdletBinding()]
param(
[Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
[Parameter(Position=1, Mandatory=$true)] [PSCredential]$Credentials,
[Parameter(Position=2, Mandatory=$true)] [string]$OutputFileName
)
Begin
{
$KQL = "kind:email OR kind:meetings OR kind:contacts OR kind:tasks OR kind:notes OR kind:IM OR kind:rssfeeds OR kind:voicemail";
$SearchableMailboxString = $MailboxName;

## 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
}

## Set Exchange Version
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_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
$creds = New-Object System.Net.NetworkCredential($Credentials.UserName.ToString(),$Credentials.GetNetworkCredential().password.ToString())
$service.Credentials = $creds
#$service.TraceEnabled = $true
#Credentials Option 2
#service.UseDefaultCredentials = $true

## Choose to ignore any SSL Warning issues caused by Self Signed Certificates

## 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

## 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})
"Using CAS Server : " + $Service.url

#CAS URL Option 2 Hardcoded

#$uri=[system.URI] "https://casservername/ews/exchange.asmx"
#$service.Url = $uri

## Optional section for Exchange Impersonation

#$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)

$gsMBResponse = $service.GetSearchableMailboxes($SearchableMailboxString, $false);
$msbScope = New-Object Microsoft.Exchange.WebServices.Data.MailboxSearchScope[] $gsMBResponse.SearchableMailboxes.Length
$mbCount = 0;
foreach ($sbMailboxin$gsMBResponse.SearchableMailboxes)
{
$msbScope[$mbCount] = New-Object Microsoft.Exchange.WebServices.Data.MailboxSearchScope($sbMailbox.ReferenceId, [Microsoft.Exchange.WebServices.Data.MailboxSearchLocation]::All);
$mbCount++;
}
$smSearchMailbox = New-Object Microsoft.Exchange.WebServices.Data.SearchMailboxesParameters
$mbq = New-Object Microsoft.Exchange.WebServices.Data.MailboxQuery($KQL, $msbScope);
$mbqa = New-Object Microsoft.Exchange.WebServices.Data.MailboxQuery[] 1
$mbqa[0] = $mbq
$smSearchMailbox.SearchQueries = $mbqa;
$smSearchMailbox.PageSize = 100;
$smSearchMailbox.PageDirection = [Microsoft.Exchange.WebServices.Data.SearchPageDirection]::Next;
$smSearchMailbox.PerformDeduplication = $false;
$smSearchMailbox.ResultType = [Microsoft.Exchange.WebServices.Data.SearchResultType]::StatisticsOnly;
$srCol = $service.SearchMailboxes($smSearchMailbox);
$rptCollection = @()

if ($srCol[0].Result -eq[Microsoft.Exchange.WebServices.Data.ServiceResult]::Success)
{
foreach($KeyWorkdStatin$srCol[0].SearchResult.KeywordStats){
if($KeyWorkdStat.Keyword.Contains(" OR ") -eq$false){
$rptObj = "" | Select ItemType,ItemHits,Size
$rptObj.ItemType = $KeyWorkdStat.Keyword.Replace("Kind:","")
$rptObj.ItemHits = $KeyWorkdStat.ItemHits
$rptObj.Size = [System.Math]::Round($KeyWorkdStat.Size /1024/1024,2)
$rptCollection += $rptObj
}
}
}
Write-Output$rptCollection
$rptCollection | Export-Csv -NoTypeInformation -Path $OutputFileName

}
}

Deleted Items Folder item type retention statistics script

$
0
0
This is a followup post to last weeks Mailbox Item type script based on a question somebody asked about the Deleted Items folder and retention of different item types. So what this script does is produces a more detailed ItemType report for all the Items in this case just in one particular folder the Deleted Items Folder. Instead of using a Search it just enumerates though all the Items in the target folder and uses some logic to calculate a few different statistics for Items in that folder. Here's the type of report it produces.


So the script reports  on
  • Total number of Items for each ItemType
  • Total Size of Items per ItemType
  • Oldest and Newest Received Date for each ItemType
  • Using the PR_RetentionDate extended property (if its set) it works out the number of days left until the Item expires and then calculates the number of Items that will expire within 7, 7-14,14-30 and over 30 days (or not at all) for each ItemType.
 To run the script you just enter the Mailbox you want to run against and the full path the Output csv file eg

 Get-DeletedItemsStats -MailboxName mailbox@domain.com -ReportFileName c:\temp\rep.csv

I've also created another version of the script that takes a -FolderPath parameter so you can run it against any folder in the mailbox

eg to run it against the clutter folder

 Get-FolderItemsStats -MailboxName mailbox@domain.com -FolderPath '\Clutter' -ReportFileName c:\temp\rep.csv

or the Junk Mail folder

 Get-FolderItemsStats -MailboxName mailbox@domain.com -FolderPath '\Junk Email' -ReportFileName c:\temp\rep.csv

I've put a download of both scripts here

The code itself looks like

####################### 
<#
.SYNOPSIS
Creates a Statistics Report of Items in the Deleted Items folder in a Mailbox using the Exchange Web Services API

.DESCRIPTION
Creates a Statistics Report of Items in the Deleted Item folder in a Mailbox using the Exchange Web Services API

Requires the EWS Managed API from https://www.microsoft.com/en-us/download/details.aspx?id=42951

.EXAMPLE
PS C:\>Get-DeletedItemsStats -MailboxName user.name@domain.com -ReportFileName c:\temp\delItemStats.csv
This Example creates a Creates a Statistics Report of Items in the Deleted Items folder in a Mailbox
#>
functionGet-DeletedItemsStats
{
[CmdletBinding()]
param(
[Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
[Parameter(Position=1, Mandatory=$true)] [PSCredential]$Credentials,
[Parameter(Position=2, Mandatory=$true)] [string]$ReportFileName
)
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
}

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

## 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

## 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

## 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})
"Using CAS Server : " + $Service.url

#CAS URL Option 2 Hardcoded

#$uri=[system.URI] "https://casservername/ews/exchange.asmx"
#$service.Url = $uri

## Optional section for Exchange Impersonation

#$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)
# Bind to Deleted Items Folder
$folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::DeletedItems,$MailboxName)
$DeletedItems = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)
#Define ItemView to retrive just 1000 Items
$ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)
$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
do{
$fiItems = $service.FindItems($DeletedItems.Id,$ivItemView)
#[Void]$service.LoadPropertiesForItems($fiItems,$psPropset)
foreach($Itemin$fiItems.Items){
#Process Item
if($rptCollection.Contains($Item.ItemClass) -eq$false){
$rptObj = "" | Select ItemClass,NumberOfItems,SizeOfItems,OldestItem,NewestItem,RetentionExpire7,RetentionExpire7To14,RetentionExpire14To30,RetentionExpire30Plus,NoRetention
$rptObj.OldestItem = $Item.datetimereceived
$rptObj.NewestItem = $Item.datetimereceived
$rptObj.ItemClass = $Item.ItemClass
$rptObj.NumberOfItems = 1
$rptObj.SizeOfItems = $Item.Size
$rptObj.RetentionExpire7 = 0
$rptObj.RetentionExpire7To14 = 0
$rptObj.RetentionExpire14To30 = 0
$rptObj.RetentionExpire30Plus = 0
$rptObj.NoRetention = 0
$RetvalObj = $null
if($Item.TryGetProperty($PR_RETENTION_DATE,[ref]$RetvalObj)){
$rDays = ($RetvalObj - (Get-Date)).Days
if($rDays-le 7){
$rptObj.RetentionExpire7++
}
if($rDays-gt 7 -band$rDays-le 14 ){
$rptObj.RetentionExpire7To14++
}
if($rDays-gt 14 -band$rDays-le 30 ){
$rptObj.RetentionExpire14To30++
}
if($rDays-gt 30){
$rptObj.RetentionExpire30Plus++
}
}
else{
$rptObj.NoRetention++
}
$rptCollection.Add($Item.ItemClass,$rptObj)
}
else{
if($Item.datetimereceived -ne$null){
if($Item.datetimereceived -gt$rptObj.NewestItem){
$rptObj.NewestItem = $Item.datetimereceived
}
if($Item.datetimereceived -lt$rptObj.OldestItem){
$rptObj.OldestItem = $Item.datetimereceived
}
}
$RetvalObj = $null
if($Item.TryGetProperty($PR_RETENTION_DATE,[ref]$RetvalObj)){
$rDays = ($RetvalObj - (Get-Date)).Days
if($rDays-le 7){
$rptCollection[$Item.ItemClass].RetentionExpire7++
}
if($rDays-gt 7 -band$rDays-le 14 ){
$rptCollection[$Item.ItemClass].RetentionExpire7To14++
}
if($rDays-gt 14 -band$rDays-le 30 ){
$rptCollection[$Item.ItemClass].RetentionExpire14To30++
}
if($rDays-gt 30){
$rptCollection[$Item.ItemClass].RetentionExpire30Plus++
}
}
else{
$rptCollection[$Item.ItemClass].NoRetention++
}
$rptCollection[$Item.ItemClass].NumberOfItems += 1
$rptCollection[$Item.ItemClass].SizeOfItems += $Item.Size
}
}
$ivItemView.Offset += $fiItems.Items.Count
}while($fiItems.MoreAvailable -eq$true)
$rptCollection.Values | Export-Csv -NoTypeInformation -Path $ReportFileName
Write-Output$rptCollection.Values
}
}

EWS Contacts rollup Powershell module

$
0
0
In this post I'm going to try to rollup a number of different Contact Scripts I've posted over the past couple of years into one module that will hopefully make things a little easier to use when you want to do things with Contacts using EWS and Powershell

I've put the code for this script up in GitHub repo https://github.com/gscales/Powershell-Scripts/blob/master/EWSContacts/EWSContactFunctions.ps1 it's 1K+ lines and I hope to add a few more things to the script as I go (and fix bugs). But if anybody has idea's please use GitHub to submit them. You can download the script as a zip from here

So here's what it can do at the moment

Get-Contact 

This can be used to get a Contact from the Mailbox's default contacts folder, other contacts sub folder or the Global Address List eg to get a contact from the default contact folder by searching using the Email Address (This will return a EWS Managed API Contact object).

Example 1

Get-Contact -MailboxName mailbox@domain.com -EmailAddress contact@email.com

This will search the default contacts folder using the ResolveName operation in EWS, it also caters for contacts that where added from the Global Address List in Outlook. When you add a contact from the GAL the email address that is stored in the Mailbox's contacts Folder uses the EX Address format. So in this case when you go to resolve or search on the SMTP address it won't find the contact that has been added from the GAL with this address. To cater for this the GAL is also searched for the EmailAddress you enter in (using ResolveName), if a GAL entry is returned (that has a matching EmailAddress) then the EX Address is obtained using Autodiscover and the UserDN property and then another Resolve is done against the Contacts Folder using the EX address.

Because ResolveName allows you to resolve against more then just the Email address I've added a -Partial Switch so you can also do partial match searches. Eg to return all the contacts that contain a particular word (note this could be across all the properties that are searched) you can use

Get-Contact -MailboxName mailbox@domain.com -EmailAddress glen -Partial

By default only the Primary Email of a contact is checked when you using ResolveName if you want it to search the multivalued Proxyaddressses property you need to use something like the following

Get-Contact -MailboxName  mailbox@domain.com -EmailAddress smtp:info@domain.com -Partial

Or to search via the SIP address you can use

Get-Contact -MailboxName  mailbox@domain.com -EmailAddress sip:info@domain.com -Partial

(using the Partial switch is required in this case because the EmailAddress your search on won't match the PrimaryAddress of the contact so in this case also you can get partial matches back).

There is also a -SearchGal switch for this cmdlet which means only the GAL is searched eg

Get-Contact -MailboxName mailbox@domain.com -EmailAddress gscales@domain.com -SearchGal

In this case the contact object returned will be read only (although you can save it into a contacts folder which I've used in another cmdlet).

Finally if your contacts aren't located in the default contacts folder you can use the folder parameter to enter in the path to folder that you want to search eg

Get-Contact -MailboxName mailbox@domain.com -EmailAddress gscales@domain.com -Folder "\Contacts\SubFolder"

Create-Contact

This can be used to create a contact, I've added parameters for all the most common properties you would set in a contact, some contact property need to be set via Extended properties (if you need to set this you can either add it in yourself or after you create the contact use Get-Contact and update the contact object).

Example 1 to create a contact in the default contacts folder

Create-Contact -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -FirstName John -LastName Doe -DisplayName "John Doe"

to create a contact and add a contact picture

Create-Contact -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -FirstName John -LastName Doe -DisplayName "John Doe" -photo 'c:\photo\Jdoe.jpg'

to create a contact in a user created subfolder

 Create-Contact -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -FirstName John -LastName Doe -DisplayName "John Doe" -Folder "\MyCustomContacts"

This cmdlet uses the EmailAddress as unique key so it wont let you create a contact with that email address if one already exists.

Update-Contact

This Cmdlet can be used to update an existing contact the Primary email address is used as a unique key so this is the one property you can't update. It will take the Partial switch like the other cmdlet but will always prompt before updating in this case.

Example1 update the phone number of an existing contact

Update-Contact  -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -MobilePhone 023213421

Example 2 update the phone number of a contact in a users subfolder

Update-Contact  -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -MobilePhone 023213421 -Folder "\MyCustomContacts"

Delete-Contact

This Cmdlet can be used to delete a contact from a contact folders

eg to delete a contact from the default contacts folder

Delete-Contact -MailboxName mailbox@domain.com -EmailAddress email@domain.com

to delete a contact from a non user subfolder

Delete-Contact -MailboxName mailbox@domain.com -EmailAddress email@domain.com -Folder \Contacts\Subfolder

Export-Contact

This cmdlet can be used to export a contact to a VCF file, this takes advantage of EWS ability to provide the contact as a VCF stream via the MimeContent property.

To export a Contact to a vcf use

Export-Contact -MailboxName mailbox@domain.com -EmailAddress address@domain.com -FileName c:\export\filename.vcf

If the file already exists it will handle creating a unique filename

To export from a contacts subfolder use

Export-Contact -MailboxName mailbox@domain.com -EmailAddress address@domain.com -FileName c:\export\filename.vcf -folder \contacts\subfolder

Export-GALContact

This cmdlet exports a Global Address List entry to a VCF file, unlike the Export-Contact cmdlet which can take advantage of the MimeStream provided by the Exchange Store with GAL Contact you don't have this available. The script creates aVCF file manually using the properties returned from ResolveName. By default the GAL photo is included with the file but I have included a -IncludePhoto switch which will use the GetUserPhoto operation which is only available on 2013 and greater.

Example 1 to save a GAL Entry to a vcf

Export-GalContact -MailboxName user@domain.com -EmailAddress email@domain.com -FileName c:\export\export.vcf

Example 2 to save a GAL Entry to vcf including the users photo

Export-GalContact -MailboxName user@domain.com -EmailAddress email@domain.com -FileName c:\export\export.vcf -IncludePhoto

Copy-Contacts.GalToMailbox

This Cmdlet copies a contact from the Global Address list to a local contacts folder. The EmailAddress in used as a unique key so the same contact won't be copied into a local contacts folder if it already exists. By default the GAL photo isn't included but I have included a -IncludePhoto switch which will use the GetUserPhoto operation which is only available on 2013 and greater.

Example 1 to Copy a Gal contacts to local Contacts folder

Copy-Contacts.GalToMailbox -MailboxName mailbox@domain.com -EmailAddress email@domain.com

Example 2 Copy a GAL contact to a Contacts subfolder

Copy-Contacts.GalToMailbox -MailboxName mailbox@domain.com -EmailAddress email@domain.com  -Folder \Contacts\UnderContacts

Advanced Search Folder creation with EWS and Powershell

$
0
0
In the past I've posted some examples of creating SearchFolders in Exchange with EWS and Powershell in the following post,  in this post I want to cover some more advanced things you can do with EWS and Search Folders.

The first thing I want to cover is how you change the option on a Search folder from "Show number of unread items" which is the default when you create a search folder in EWS to "Show total number of items". eg




To change this option you need to set the PidTagExtendedFolderFlags property which is documented in http://msdn.microsoft.com/en-us/library/office/cc839880(v=office.15).aspx . This is a binary property and you set this particular option by changing bit 6-7 in this property. Because you also need the SearchFolderGuid in this property the easiest time is to set this is right after you create the folder. eg
  1. $searchFolder.Save($sfRoot.Id);  
  2. $PR_EXTENDED_FOLDER_FLAGS = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x36DA, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)  
  3. $psPropset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)    
  4. $psPropset.Add($PR_EXTENDED_FOLDER_FLAGS)  
  5. $searchFolder.Load($psPropset)  
  6. $exVal = $null  
  7. if($SearchFolder.TryGetProperty($PR_EXTENDED_FOLDER_FLAGS,[ref]$exVal)){  
  8.      $newVal = "010402001000" + [System.BitConverter]::ToString($exVal).Replace("-","")  
  9.      $byteVal = HexStringToByteArray($newVal)  
  10.      $SearchFolder.SetExtendedProperty($PR_EXTENDED_FOLDER_FLAGS,$byteVal)  
  11.      $searchFolder.Update()  
  12. }  
  13. "Folder Created"  

Some other interesting things you can do when using EWS to create search Folders is you can filter on some of the more advanced Item properties you can't access normally using Outlook. For example the PidTagLastVerbExecuted can be used to create a Search folder to find all email where the last action taken on the Message was to reply to the Sender. Eg this would create a SearchFolder with this type of filter
  1. $PR_LAST_VERB_EXECUTED = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x1081, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer)    
  2. $sfItemSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($PR_LAST_VERB_EXECUTED,102)   
  3. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  4. $SearchFolder = new-object Microsoft.Exchange.WebServices.Data.SearchFolder($service);  
  5. $rfRootFolderId = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)     
  6. $searchFolder.SearchParameters.RootFolderIds.Add($rfRootFolderId);  
  7. $searchFolder.SearchParameters.Traversal = [Microsoft.Exchange.WebServices.Data.SearchFolderTraversal]::Deep;  
  8. $searchFolder.SearchParameters.SearchFilter = $sfItemSearchFilter  
  9. $searchFolder.DisplayName = $SearchFilterName  
  10. $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::SearchFolders,$MailboxName)     
  11. $sfRoot = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)  
  12. $searchFolder.Save($sfRoot.Id);  

Another interesting Search Folder you can create on 2013/Office365 is using the LatestWordCount property which isn't a documented property but you can use it to produce a SearchFolder of all your wordy emails eg the following would create a SearchFolder of email where this property indicated the word count of the message was greater than 1000 words.
  1. [Guid]$psguid = "23239608-685D-4732-9C55-4C95CB4E8E33"  
  2. $LatestMessageWordCount = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition($psguid,"LatestMessageWordCount", [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer)    
  3. $sfItemSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThan($LatestMessageWordCount,1000)   
  4. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  5. $SearchFolder = new-object Microsoft.Exchange.WebServices.Data.SearchFolder($service);  
  6. $rfRootFolderId = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)     
  7. $searchFolder.SearchParameters.RootFolderIds.Add($rfRootFolderId);  
  8. $searchFolder.SearchParameters.Traversal = [Microsoft.Exchange.WebServices.Data.SearchFolderTraversal]::Deep;  
  9. $searchFolder.SearchParameters.SearchFilter = $sfItemSearchFilter  
  10. $searchFolder.DisplayName = $SearchFilterName  
  11. $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::SearchFolders,$MailboxName)     
  12. $sfRoot = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)  
  13. $searchFolder.Save($sfRoot.Id);  

I've put a download of full sample scripts for creating search folder for these two examples here

EWS Outlook Quick Steps Powershell how to

$
0
0
Quickstep's in Outlook is a feature that was added in Outlook in 2010 to help automate repetitive tasks you do with Email in Outlook. In this Post I'm going to go through how you can manipulate the objects associated with Quick steps using EWS and PowerShell.

How Quick Steps are implemented in a Mailbox

The OuickSteps Folder is created by Outlook (eg no Outlook no folder) In the Mailbox's IPM Root as a hidden folder called Quick Steps (with a FolderClass of IPF.Configuration) and within this folder there are FAI (Folder Associated Items) which represent each of the QuickStep Items. On each of those there is a PidTagRoamingXmlStream property which contains a ByteArray that represents an XMLDocument for each of the QuickStep items for example they look something like


<?xml version="1.0"?>
<CombinedActionOrdinal="3200"Tooltip=""Icon="MoveToFolder"Name="Clutter"Version="147153">
<ActionMoveToFolder>
<Folder>80010....06F006D0000000000</Folder>
</ActionMoveToFolder>
<ActionMarkAsRead>
</ActionMarkAsRead>
</CombinedAction>

Getting the QuickSteps Folder

To Get the QuickSteps folder (if it exists) you can use the PidTagAdditionalRenEntryIdsEx property on the IPM Root folder which will contain the HexEntryId of the QuickSteps folder in the RSF_PID_COMBINED_ACTIONS PersistBlockType value. Eg you can do this using the following code

functionGet-QuickStepsFolder
{
param(
[Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
[Parameter(Position=1, Mandatory=$true)] [PSCredential]$Credentials,
[Parameter(Position=2, Mandatory=$false)] [Microsoft.Exchange.WebServices.Data.ExchangeService]$service
)
Begin
{
if(!$service){
$service = Connect-Exchange -MailboxName $MailboxName -Credential $Credentials
}
$PidTagAdditionalRenEntryIdsEx = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x36D9, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)
$psPropset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$psPropset.Add($PidTagAdditionalRenEntryIdsEx)
$folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,$MailboxName)
$IPM_ROOT = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid,$psPropset)
$binVal = $null;
$AdditionalRenEntryIdsExCol = @{}
if($IPM_ROOT.TryGetProperty($PidTagAdditionalRenEntryIdsEx,[ref]$binVal)){
$hexVal = [System.BitConverter]::ToString($binVal).Replace("-","");
##Parse Binary Value first word is Value type Second word is the Length of the Entry
$Sval = 0;
while(($Sval+8) -lt$hexVal.Length){
$PtypeVal = $hexVal.SubString($Sval,4)
$PtypeVal = $PtypeVal.SubString(2,2) + $PtypeVal.SubString(0,2)
$Sval +=12;
$PropLengthVal = $hexVal.SubString($Sval,4)
$PropLengthVal = $PropLengthVal.SubString(2,2) + $PropLengthVal.SubString(0,2)
$PropLength = [Convert]::ToInt64($PropLengthVal, 16)
$Sval +=4;
$ProdIdEntry = $hexVal.SubString($Sval,($PropLength*2))
$Sval += ($PropLength*2)
#$PtypeVal + " : " + $ProdIdEntry
$AdditionalRenEntryIdsExCol.Add($PtypeVal,$ProdIdEntry)
}
}
$QuickStepsFolder = $null
if($AdditionalRenEntryIdsExCol.ContainsKey("8007")){
$siId = ConvertFolderid -service $service -MailboxName $MailboxName -hexid $AdditionalRenEntryIdsExCol["8007"]
$QuickStepsFolderId = new-object Microsoft.Exchange.WebServices.Data.FolderId($siId.UniqueId.ToString())
$QuickStepsFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$QuickStepsFolderId)
}
else{
Write-Host ("QuickSteps folder not found")
throw ("QuickSteps folder not found")
}
return$QuickStepsFolder


}

}

Getting the Quick Steps Items

The Quick Step items are stored in the Folder Associated Items collection of the Quick Steps folder. To get these using EWS after you get the Quick Steps folder you just need to enumerate the items in the FAI collection by specifying an Associated Items traversal. eg the following code will return the existing Quick Step items

functionGet-ExistingSteps{
param(
[Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
[Parameter(Position=1, Mandatory=$true)] [Microsoft.Exchange.WebServices.Data.Folder]$QuickStepsFolder
)
Begin
{
$NameList = @{}
$enc = [system.Text.Encoding]::ASCII
$PR_ROAMING_XMLSTREAM = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x7C08,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$psPropset.Add($PR_ROAMING_XMLSTREAM)
#Define ItemView to retrive just 1000 Items
$ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)
$ivItemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Associated
$fiItems = $null
do{
$fiItems = $QuickStepsFolder.FindItems($ivItemView)
if($fiItems.Items.Count -gt 0){
[Void]$service.LoadPropertiesForItems($fiItems,$psPropset)
foreach($Itemin$fiItems.Items){
$propval = $null
if($Item.TryGetProperty($PR_ROAMING_XMLSTREAM,[ref]$propval)){
[XML]$xmlVal = $enc.GetString($propval)
if(!$NameList.ContainsKey($xmlVal.CombinedAction.Name.ToLower())){
$NameList.Add($xmlVal.CombinedAction.Name.Trim().ToLower(),$Item)
}
}
}
}
$ivItemView.Offset += $fiItems.Items.Count
}while($fiItems.MoreAvailable -eq$true)
return$NameList
}
}

Export QuickStep XML

To Export the XML from a Quick Step Item you can grab the PR_Roaming_XMLStream property and then save this out to a file (it just plain XML) eg

functionExport-QuickStepXML{
param(
[Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
[Parameter(Position=1, Mandatory=$true)] [PSCredential]$Credentials,
[Parameter(Position=2, Mandatory=$true)] [string]$Name,
[Parameter(Position=3, Mandatory=$true)] [string]$FileName
)
Begin{
#Connect
$service = Connect-Exchange -MailboxName $MailboxName -Credential $Credentials
$QuickStepsFolder = Get-QuickStepsFolder -MailboxName $MailboxName -service $service -Credentials $Credentials
$ExistingSteps = Get-ExistingSteps -MailboxName $MailboxName -QuickStepsFolder $QuickStepsFolder
if($ExistingSteps.ContainsKey($Name.Trim().ToLower())){
$PR_ROAMING_XMLSTREAM = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x7C08,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$psPropset.Add($PR_ROAMING_XMLSTREAM)
$propval = $null
if($ExistingSteps[$Name.Trim().ToLower()].TryGetProperty($PR_ROAMING_XMLSTREAM,[ref]$propval)){
[System.IO.File]::WriteAllBytes($FileName,$propval)
Write-Host ('Exported to ' + $FileName)
}
}
}
}

Creating a QuickStep from XML (Unsupported)

Note creating Quicksteps using anything other then Outlook should be considered unsupported so if you use this make sure you do your own testing. Creating a QuickStep from the XML is just a matter of creating an Item and setting the PR_Roaming_XMLStream property with the appropriate XML from the item. The following cmdlet first reads the existing Quicksteps to ensure your creating a Quickstep with the same name as an existing one. eg

functionExport-QuickStepXML{
param(
[Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
[Parameter(Position=1, Mandatory=$true)] [PSCredential]$Credentials,
[Parameter(Position=2, Mandatory=$true)] [string]$Name,
[Parameter(Position=3, Mandatory=$true)] [string]$FileName
)
Begin{
#Connect
$service = Connect-Exchange -MailboxName $MailboxName -Credential $Credentials
$QuickStepsFolder = Get-QuickStepsFolder -MailboxName $MailboxName -service $service -Credentials $Credentials
$ExistingSteps = Get-ExistingSteps -MailboxName $MailboxName -QuickStepsFolder $QuickStepsFolder
if($ExistingSteps.ContainsKey($Name.Trim().ToLower())){
$PR_ROAMING_XMLSTREAM = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x7C08,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);
$psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$psPropset.Add($PR_ROAMING_XMLSTREAM)
$propval = $null
if($ExistingSteps[$Name.Trim().ToLower()].TryGetProperty($PR_ROAMING_XMLSTREAM,[ref]$propval)){
[System.IO.File]::WriteAllBytes($FileName,$propval)
Write-Host ('Exported to ' + $FileName)
}
}
}
}

I've put all these samples into a QuickSteps EWS module which you can download from here some examples of what you can do with this

to Get the QuickSteps folder

Get-QuickStepsFolder -MailboxName mailbox@domain.com

To Get the existing Quick Steps

 Get-QuickSteps -MailboxName mailbox@domain.com

This returns a HashTable of the QuickSteps to access a Quickstep within the collection use the Index value eg
 $QuickSteps = Get-QuickSteps -MailboxName mailbox@domain.com
 $QuickSteps["clutter"]

To Delete and existing QuickStep

Delete-QuickStep -MailboxName gscales@datarumble.com -Name 'Task Name'

This cmdlet will search for a quickstep based on the Name attribute in the QuickSteps XML document if one is found it will prompt if you want to delete it.

Text Emoticon Outlook and OWA Compose App

$
0
0
Emojis and emoticons have been around for quite a while but are picking up a bit of steam of late with the release of Windows 10. The Daily mail had a really good article on their use recently which is worth a read.

Like everything these new emoji's have there origins story and text based emoticons (which have many other names) are kind of a interesting sub branch with a slightly retro feel.

In this post I'm going to show you how you can create a Mail App to make using text emoticon's easy in Exchange 2013 and Office365 using the new compose apps feature which was introduced last year at MEC.

Compose apps are one way of easily extending the User interface in OWA and Outlook 2013 (and 2016). So for this example I can do something like the following to make these text emoticons easy to insert into either the Subject or Body of a message (you need to active the app by selecting the Addin Button in OWA or Apps for Office in Outlook)


At this point it should be noted the Unicode support in Email subjects is a relative new thing the RFC came out in 1996 https://tools.ietf.org/html/rfc2047 full Unicode support in Outlook didn't really happen until 2003. So if your sending to someone still rocking old versions be warned actually because of the difference in the implementation of character sets and available between different platforms windows/android/ios etc some of these text base emoticons may only display correctly on some platforms/browsers/mail clients. But you can then have hours of fun filling you mailbox with this type of stuff



Mail Apps are relatively simple to implement and this is a pretty straight forward example the first thing you have is the Manifest file where all the configuration information is held for this Mail App. Eg in my app some of the important relative information is

<FormSettings>
<Formxsi:type="ItemEdit">
<DesktopSettings>
<SourceLocationDefaultValue=
"https://gscales.github.io/TextEmoticon/index.html"/>
</DesktopSettings>
<TabletSettings>
<SourceLocationDefaultValue=
"https://gscales.github.io/TextEmoticon/index.html"/>
</TabletSettings>
<PhoneSettings>
<SourceLocationDefaultValue=
"https://gscales.github.io/TextEmoticon/index.html"/>
</PhoneSettings>
</Form>
</FormSettings>

This tells us where the Mail App is being hosted is my case just in my GitHub Repo to learn more about the other manifest setting have a read of this. The Mail App itself is just a pretty simple html and javascript app that creates a table and reads in the text emoticons and controls the formatting and takes care of the backend operations by hooking into the JavaScript Api for office.

In the script.js for this app when you select the Subject object and click a text emoticon to insert your chosen Icon it will append this to the current subject (there is no way to do a positional insert based on the current position of the cursor in the subject. For the body you can insert the Icon at the current cursor location using the method from https://msdn.microsoft.com/EN-US/library/office/dn574748.aspx#mod_off15_HowToInsertDataBody_AtCursor

I've posted the code for this up into my GitHub repo at https://github.com/gscales/gscales.github.io/tree/master/TextEmoticon  if you want to test it you can install the app from the manifest at  https://gscales.github.io/TextEmoticon/manifest.xml . Although I'd recommend you clone it yourself and then you can change the text emoticons in the script.js file to whatever you like. If you do clone the repository you need to change the (the source location settings shown above) settings in the Manifest file to ensure its pointing at the files your hosting or you will never see your changes take affect.

Reporting on expired recurring meetings with EWS and Powershell

$
0
0
Someone asked an interesting question about recurring Appointments in Exchange recently that I thought I'd expand on a bit with blog post. Recurrence in Appointments,Meeting and Tasks are one of the more useful features of Exchange, but they are also one the features that are harder for developers and anybody writing code or scripts to get a handle on. These days there is some really good technical documentation on this so I won't explain too much but firstly http://msdn.microsoft.com/EN-US/library/office/dn727655(v=exchg.150).aspx but also the Exchange Protocol Document http://msdn.microsoft.com/en-us/library/ee201188(v=exchg.80).aspx are a must to read.

To refine this down a bit with Calendar Appointments if you have a recurring Appointment you don't have separate objects for each occurrence of the recurring Appointment, the recurrence is just a pattern stored in a property on what is called the master instance of that particular Appointment, Meeting or Task. Exception information is stored as part of the Recurrence pattern (while the actual data from the Exception if there is anything different from the Master instance eg say you add an attachment to an exception this data gets stored as AttachedItem on the Master instance).

What this all means is that when it comes time to actually query the Calendar Appointments these recurrence patterns need to be expanded. To do this in EWS you use the CalendarView class and you specify a Start and End Date for your query. Exchange will then do all the hard work for you and return the expanded Appointments (and exceptions) to you.

To get back to the actual question however of how you would find expired recurring Calendar Appointments you have to forget about making an expansion query and instead you want to make a query to return a list of these master instances of recurring calendar Appointments and then filter these at the client side to work out what has expired (or if you want to do other reporting like seeing who has made non expiring appointments you can do this).

So to do this you can use the FindItem operation with a restriction on the PidLidRecurring property http://msdn.microsoft.com/en-us/library/ee157994(v=EXCHG.80).aspx which looks like this

  1. $Recurring = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Appointment, 0x8223,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Boolean);   
  2.   
  3. $sfItemSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($Recurring,$true)   

This will then return a list of Master Appointment instances, to get the full details of the Recurrence of each appointment you need to do a GetItem on each of the master instance which in the Managed API you can use the LoadPropertiesForItems method for. This will make a batch GetItem request to the Exchange Server to return the detail on all Items your request it for.

Once you have the recurrence information you can then filter those Meetings that have an End Date (or Recurrence number) set by using the HasEnd property.  Then to check the Last Occurrence you can use the LastOccurrence property. If you want to report on other things like meetings that don't have an EndDate or meetings that are expiring in the next week or month this is where you can put your own customization.

The following script will produce a report of any expired Meetings in a Mailbox's calendar and save this to a CSV file. I've put a download of this script here the code itself looks like

  1. ## Get the Mailbox to Access from the 1st commandline argument  
  2.   
  3. $MailboxName = $args[0]  
  4.   
  5. ## Load Managed API dll    
  6. Add-Type -Path "C:\Program Files\Microsoft\Exchange\Web Services\2.1\Microsoft.Exchange.WebServices.dll"    
  7.     
  8. ## Set Exchange Version    
  9. $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2    
  10.     
  11. ## Create Exchange Service Object    
  12. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
  13.     
  14. ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials    
  15.     
  16. #Credentials Option 1 using UPN for the windows Account    
  17. $psCred = Get-Credential    
  18. $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())    
  19. $service.Credentials = $creds        
  20.     
  21. #Credentials Option 2    
  22. #service.UseDefaultCredentials = $true    
  23.     
  24. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  25.     
  26. ## Code From http://poshcode.org/624  
  27. ## Create a compilation environment  
  28. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  29. $Compiler=$Provider.CreateCompiler()  
  30. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  31. $Params.GenerateExecutable=$False  
  32. $Params.GenerateInMemory=$True  
  33. $Params.IncludeDebugInformation=$False  
  34. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  35.   
  36. $TASource=@' 
  37.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  38.     public class TrustAll : System.Net.ICertificatePolicy { 
  39.       public TrustAll() {  
  40.       } 
  41.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  42.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  43.         System.Net.WebRequest req, int problem) { 
  44.         return true; 
  45.       } 
  46.     } 
  47.   } 
  48. '@   
  49. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  50. $TAAssembly=$TAResults.CompiledAssembly  
  51.   
  52. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  53. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  54. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  55.   
  56. ## end code from http://poshcode.org/624  
  57.     
  58. ## 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    
  59.     
  60. #CAS URL Option 1 Autodiscover    
  61. $service.AutodiscoverUrl($MailboxName,{$true})    
  62. "Using CAS Server : " + $Service.url     
  63.      
  64. #CAS URL Option 2 Hardcoded    
  65.     
  66. #$uri=[system.URI] "https://casservername/ews/exchange.asmx"    
  67. #$service.Url = $uri      
  68.     
  69. ## Optional section for Exchange Impersonation    
  70.     
  71. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  72.   
  73. # Bind to the Calendar Folder  
  74. $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar,$MailboxName)     
  75. $Calendar = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)  
  76.   
  77. $Recurring = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Appointment, 0x8223,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Boolean);   
  78.   
  79. $sfItemSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($Recurring,$true)   
  80.   
  81.   
  82. #Define ItemView to retrive just 1000 Items      
  83. $ivItemView =  New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)     
  84. $rptCollection = @()  
  85. $fiItems = $null      
  86. do{   
  87.     $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)    
  88.     $fiItems = $service.FindItems($Calendar.Id,$sfItemSearchFilter,$ivItemView)     
  89.     if($fiItems.Items.Count -gt 0){  
  90.         [Void]$service.LoadPropertiesForItems($fiItems,$psPropset)    
  91.         foreach($Item in $fiItems.Items){   
  92.             if($Item.Recurrence.HasEnd){  
  93.                 if($Item.LastOccurrence.End -lt (Get-Date)){  
  94.                     $rptObj = "" | Select Mailbox,Organizer,AppointmentSubject,FirstOccuranceStart,FirstOccuranceEnd,LastOccuranceStart,LastOccuranceEnd  
  95.                     Write-Host "Appointment : "  $Item.Subject  
  96.                     Write-Host "Last Occurance : " $Item.LastOccurrence.Start  
  97.                     $rptObj.Mailbox = $MailboxName  
  98.                     $rptObj.Organizer = $Item.Organizer.Name  
  99.                     $rptObj.AppointmentSubject = $Item.Subject  
  100.                     $rptObj.FirstOccuranceStart = $Item.FirstOccurrence.Start  
  101.                     $rptObj.FirstOccuranceEnd = $Item.FirstOccurrence.End  
  102.                     $rptObj.LastOccuranceStart = $Item.LastOccurrence.Start  
  103.                     $rptObj.LastOccuranceEnd = $Item.LastOccurrence.End               
  104.                     $rptCollection += $rptObj  
  105.                 }  
  106.             }  
  107.         }  
  108.     }  
  109.     $ivItemView.Offset += $fiItems.Items.Count      
  110. }while($fiItems.MoreAvailable -eq $true)   
  111. $rptCollection | Export-Csv -NoTypeInformation -Path c:\Temp\$MailboxName-ExpiredRecurringMeetings.csv  
  112. Write-Host "Report written to " c:\Temp\$MailboxName-ExpiredRecurringMeetings.csv  



Getting Folder Sizes and other Stats via EWS with Powershell

$
0
0
Somebody asked a question the other week about getting all the Folder Sizes via EWS which you can do easily using the PR_MESSAGE_SIZE_EXTENDED property and FindFolders operation (you can also get the folder size's using the Exchange Management Shell via Get-MailboxFolderStatistics cmdlet). But there is some other interesting stuff you can get via EWS that you can't from the EMS cmdlet such as the Default FolderClass (eg the PR_CONTAINER_CLASS_W http://msdn.microsoft.com/en-us/library/office/cc839839(v=office.15).aspx) which will tell you what items are being stored in that particular Folder (although as documented it's not a mandatory property although its absence in the past has caused problem in OWA etc). Another property that looks interesting but doesn't seem to be well documented is the PR_ATTACH_ON_NORMAL_MSG_COUNT which appears to the be the total number of attachments on messages in that folder including seemly inline attachment (Note I can't confirm this as it doesn't appear to be documented and can only give anecdotal test results so do your own testing if your interested in this).

So with all these interesting properties you can put together a different type of Mailbox statistics script that will grab Folder stats by FolderClass and show the following info about a mailbox


I've put a download of this script here to run this script enter the EmailAddress of the mailbox you want to run it against and It will will output a CSV to the c:\temp directory the code itself looks like

  1. ## Get the Mailbox to Access from the 1st commandline argument  
  2.   
  3. $MailboxName = $args[0]  
  4.   
  5. ## Load Managed API dll    
  6. Add-Type -Path "C:\Program Files\Microsoft\Exchange\Web Services\2.1\Microsoft.Exchange.WebServices.dll"    
  7.     
  8. ## Set Exchange Version    
  9. $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1    
  10.     
  11. ## Create Exchange Service Object    
  12. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
  13.     
  14. ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials    
  15.     
  16. #Credentials Option 1 using UPN for the windows Account    
  17. $psCred = Get-Credential    
  18. $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())    
  19. $service.Credentials = $creds        
  20.     
  21. #Credentials Option 2    
  22. #service.UseDefaultCredentials = $true    
  23.     
  24. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  25.     
  26. ## Code From http://poshcode.org/624  
  27. ## Create a compilation environment  
  28. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  29. $Compiler=$Provider.CreateCompiler()  
  30. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  31. $Params.GenerateExecutable=$False  
  32. $Params.GenerateInMemory=$True  
  33. $Params.IncludeDebugInformation=$False  
  34. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  35.   
  36. $TASource=@' 
  37.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  38.     public class TrustAll : System.Net.ICertificatePolicy { 
  39.       public TrustAll() {  
  40.       } 
  41.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  42.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  43.         System.Net.WebRequest req, int problem) { 
  44.         return true; 
  45.       } 
  46.     } 
  47.   } 
  48. '@   
  49. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  50. $TAAssembly=$TAResults.CompiledAssembly  
  51.   
  52. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  53. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  54. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  55.   
  56. ## end code from http://poshcode.org/624  
  57.     
  58. ## 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    
  59.     
  60. #CAS URL Option 1 Autodiscover    
  61. $service.AutodiscoverUrl($MailboxName,{$true})    
  62. "Using CAS Server : " + $Service.url     
  63.      
  64. #CAS URL Option 2 Hardcoded    
  65.     
  66. #$uri=[system.URI] "https://casservername/ews/exchange.asmx"    
  67. #$service.Url = $uri      
  68.     
  69. ## Optional section for Exchange Impersonation    
  70.     
  71. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  72. function ConvertToString($ipInputString){    
  73.     $Val1Text = ""    
  74.     for ($clInt=0;$clInt -lt $ipInputString.length;$clInt++){    
  75.             $Val1Text = $Val1Text + [Convert]::ToString([Convert]::ToChar([Convert]::ToInt32($ipInputString.Substring($clInt,2),16)))    
  76.             $clInt++    
  77.     }    
  78.     return $Val1Text    
  79. }   
  80.   
  81. $FolderClassrpt = @{}  
  82. function GetFolderSizes{  
  83.     param (  
  84.             $rootFolderId = "$( throw 'rootFolderId is a mandatory Parameter' )"  
  85.           )  
  86.     process{  
  87.     #Define Extended properties    
  88.     $PR_FOLDER_TYPE = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(13825,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);    
  89.     $PR_MESSAGE_SIZE_EXTENDED = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(3592, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Long);  
  90.     $folderidcnt = $rootFolderId  
  91.     #Define the FolderView used for Export should not be any larger then 1000 folders due to throttling    
  92.     $fvFolderView =  New-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)    
  93.     #Deep Transval will ensure all folders in the search path are returned    
  94.     $fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep;    
  95.     $psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)    
  96.     $PR_Folder_Path = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(26293, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  97.     $PR_ATTACH_ON_NORMAL_MSG_COUNT = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x66B1, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Long);  
  98.     #Add Properties to the  Property Set    
  99.     $psPropertySet.Add($PR_Folder_Path);    
  100.     $psPropertySet.Add($PR_MESSAGE_SIZE_EXTENDED)  
  101.     $psPropertySet.Add($PR_ATTACH_ON_NORMAL_MSG_COUNT)  
  102.     $fvFolderView.PropertySet = $psPropertySet;    
  103.     #The Search filter will exclude any Search Folders    
  104.     $sfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($PR_FOLDER_TYPE,"1")    
  105.     $fiResult = $null    
  106.     #The Do loop will handle any paging that is required if there are more the 1000 folders in a mailbox    
  107.     do {    
  108.         $fiResult = $Service.FindFolders($folderidcnt,$sfSearchFilter,$fvFolderView)    
  109.         foreach($ffFolder in $fiResult.Folders){  
  110.             $foldpathval = $null    
  111.             #Try to get the FolderPath Value and then covert it to a usable String     
  112.             if ($ffFolder.TryGetProperty($PR_Folder_Path,[ref] $foldpathval))    
  113.             {    
  114.                 $binarry = [Text.Encoding]::UTF8.GetBytes($foldpathval)    
  115.                 $hexArr = $binarry | ForEach-Object { $_.ToString("X2") }    
  116.                 $hexString = $hexArr -join ''    
  117.                 $hexString = $hexString.Replace("FEFF""5C00")    
  118.                 $fpath = ConvertToString($hexString)    
  119.             }   
  120.             $folderSize = $null  
  121.             [Void]$ffFolder.TryGetProperty($PR_MESSAGE_SIZE_EXTENDED,[ref] $folderSize)  
  122.             [Int64]$attachcnt = 0  
  123.             [Void]$ffFolder.TryGetProperty($PR_ATTACH_ON_NORMAL_MSG_COUNT,[ref] $attachcnt)  
  124.             if($attachcnt -eq $null){  
  125.                 $attachcnt = 0  
  126.             }  
  127.             "FolderPath : " + $fpath + " : " + $folderSize  
  128.             $fldClass = $ffFolder.FolderClass  
  129.             if($fldClass -eq $null){$fldClass = "IPF.Note"}  
  130.             if($FolderClassrpt.ContainsKey($fldClass)){  
  131.                 $FolderClassrpt[$fldClass].NumberOfFolders += 1  
  132.                 $FolderClassrpt[$fldClass].AttachOnMsgCount += $attachcnt  
  133.                 $FolderClassrpt[$fldClass].ItemSize += [Int64]$folderSize  
  134.                 $FolderClassrpt[$fldClass].ItemCount += [Int64]$ffFolder.TotalCount  
  135.             }  
  136.             else{  
  137.                 $rptObj = "" | select FolderClass,NumberOfFolders,AttachOnMsgCount,ItemSize,ItemCount  
  138.                 $rptObj.FolderClass = $fldClass  
  139.                 $FolderClassrpt[$fldClass].NumberOfFolders  
  140.                 $rptObj.ItemSize = [Int64]$folderSize  
  141.                 $rptObj.ItemCount = [Int64]$ffFolder.TotalCount  
  142.                 $rptObj.AttachOnMsgCount += $attachcnt  
  143.                 $rptObj.NumberOfFolders = 1  
  144.                 $FolderClassrpt.Add($fldClass,$rptObj)  
  145.             }  
  146.         }   
  147.         $fvFolderView.Offset += $fiResult.Folders.Count  
  148.     }while($fiResult.MoreAvailable -eq $true)    
  149.     }  
  150. }  
  151. GetFolderSizes -rootFolderId (new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName))     
  152.   
  153. $FolderClassrpt.Values | select FolderClass,NumberOfFolders,AttachOnMsgCount,ItemCount,@{label="TotalSize(MB)";expression={[math]::Round($_.ItemSize/1MB,2)}}  | export-csv c:\temp\$MailboxName-fldsizebyclass.csv -NoTypeInformation   


Sent and Received Time on a Message and EWS

$
0
0
This one has come up a couple of times for me over the last couple of weeks so I thought I'd put together a post to expand on the Subject a little.

The easiest place to start before talking about Exchange is to look at a ordinary MIME message and its associated headers. In a MIME message there is one Message date  header http://tools.ietf.org/html/rfc4021#section-2.1.1 which outlines "Specifies the date and time at which the creator of the message indicated that the message was complete and ready to enter the mail delivery system" which is a way of saying that its the Sent Time. eg In a Message

Subject: Re: Oh no
Date: Thu, 14 Aug 2014 18:44:52 +1000
Message-ID: <nk0h69wn6waj8s32rnc3kma0.1408005882691@email.android.com>

 As the Message traverses various MTA's along the way to its final destinations, Received headers with dates are added to the Transport Headers  of a message indicating the date\time a particular MTA's processed the message eg

Received: from BY2PR03MB459.namprd03.prod.outlook.com (10.141.141.147) by
 DM2PR03MB463.namprd03.prod.outlook.com (10.141.85.20) with Microsoft SMTP
 Server (TLS) id 15.0.859.15 via Mailbox Transport; Wed, 29 Jan 2014 22:06:05
 +0000

When the Message finally arrives at is destination and is delivered by the Store to a users Mailbox two MAPI properties will be created to  reflect the Sent Time and also the Date Time the message was delivered by the store (which should match (most recent) received header in the message). With POP3 and and some POP downloaders this is where the date can get a little offset and not represent the real time of message delivery. But sticking to Exchange the following two props get set

PR_MESSAGE_DELIVERY_TIME which in EWS is also represented by the Strongly typed DateTimeReceived property

PR_CLIENT_SUBMIT_TIME property which in EWS is represented by the Strongly typed DateTimeSent property.

Exchange when it stores these dates like with other dates will store these in UTC format. When your using Outlook with the default views it will use the PR_MESSAGE_DELIVERY_TIME for every Mail folder other then the Sent Items where the view will use the PR_CLIENT_SUBMIT_TIME.

So whenever you importing EML's and you see an unexpected Received (or Sent) by date the first thing to check is the Transport Headers and look at the most recent received header. If your missing these headers then that maybe why your dates aren't what you expect.

In EWS you can access the Transport Headers via the InternetMessageHeaders strongly typed property or you can use the PR_TRANSPORT_MESSAGE_HEADERS extended properties. Depending on the version of Exchange you are using there can be issues with the strongly typed property so you should read http://msdn.microsoft.com/EN-US/library/office/hh545614(v=exchg.140).aspx . Because of the size of these properties this information will only be returned when you use a GetItem's operation

The other thing you can do with these message dates is track the amount of time it took from submit to the delivery of a message eg the following script will use EWS to grab both the PR_MESSAGE_DELIVERY_TIME  and PR_CLIENT_SUBMIT_TIME MAPI properties and use those to calculate the delivery time is also parses the Sent Header datetime and creates a CSV of the output. I've put a download of this script here the code looks like

  1. ## Get the Mailbox to Access from the 1st commandline argument  
  2.   
  3. $MailboxName = $args[0]  
  4.   
  5. ## Load Managed API dll    
  6.   
  7. ###CHECK FOR EWS MANAGED API, IF PRESENT IMPORT THE HIGHEST VERSION EWS DLL, ELSE EXIT  
  8. $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")  
  9. if (Test-Path $EWSDLL)  
  10.     {  
  11.     Import-Module $EWSDLL  
  12.     }  
  13. else  
  14.     {  
  15.     "$(get-date -format yyyyMMddHHmmss):"  
  16.     "This script requires the EWS Managed API 1.2 or later."  
  17.     "Please download and install the current version of the EWS Managed API from"  
  18.     "http://go.microsoft.com/fwlink/?LinkId=255472"  
  19.     ""  
  20.     "Exiting Script."  
  21.     exit  
  22.     }  
  23.     
  24. ## Set Exchange Version    
  25. $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2    
  26.     
  27. ## Create Exchange Service Object    
  28. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
  29.     
  30. ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials    
  31.     
  32. #Credentials Option 1 using UPN for the windows Account    
  33. $psCred = Get-Credential    
  34. $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())    
  35. $service.Credentials = $creds        
  36.     
  37. #Credentials Option 2    
  38. #service.UseDefaultCredentials = $true    
  39.     
  40. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  41.     
  42. ## Code From http://poshcode.org/624  
  43. ## Create a compilation environment  
  44. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  45. $Compiler=$Provider.CreateCompiler()  
  46. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  47. $Params.GenerateExecutable=$False  
  48. $Params.GenerateInMemory=$True  
  49. $Params.IncludeDebugInformation=$False  
  50. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  51.   
  52. $TASource=@' 
  53.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  54.     public class TrustAll : System.Net.ICertificatePolicy { 
  55.       public TrustAll() {  
  56.       } 
  57.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  58.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  59.         System.Net.WebRequest req, int problem) { 
  60.         return true; 
  61.       } 
  62.     } 
  63.   } 
  64. '@   
  65. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  66. $TAAssembly=$TAResults.CompiledAssembly  
  67.   
  68. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  69. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  70. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  71.   
  72. ## end code from http://poshcode.org/624  
  73.     
  74. ## 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    
  75.     
  76. #CAS URL Option 1 Autodiscover    
  77. $service.AutodiscoverUrl($MailboxName,{$true})    
  78. "Using CAS Server : " + $Service.url     
  79.      
  80. #CAS URL Option 2 Hardcoded    
  81.     
  82. #$uri=[system.URI] "https://casservername/ews/exchange.asmx"    
  83. #$service.Url = $uri      
  84.     
  85. ## Optional section for Exchange Impersonation    
  86.     
  87. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  88.   
  89. # Bind to the Inbox Folder  
  90. $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)     
  91. $Inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)  
  92.   
  93. $PR_MESSAGE_DELIVERY_TIME = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0E06, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::SystemTime)  
  94. $PR_CLIENT_SUBMIT_TIME = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0039, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::SystemTime)  
  95. $PR_TRANSPORT_MESSAGE_HEADERS = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x007D,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);  
  96. $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly)    
  97. $psPropset.Add($PR_CLIENT_SUBMIT_TIME)  
  98. $psPropset.Add($PR_MESSAGE_DELIVERY_TIME)  
  99. $psPropset.Add($PR_TRANSPORT_MESSAGE_HEADERS)  
  100. $psPropset.Add([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject)  
  101. #Define ItemView to retrive just 1000 Items      
  102. $ivItemView =  New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)    
  103. $ivItemView.PropertySet = $psPropset  
  104. $rptCollection = @()  
  105. $fiItems = $null      
  106. do{      
  107.     $fiItems = $service.FindItems($Inbox.Id,$ivItemView)      
  108.     [Void]$service.LoadPropertiesForItems($fiItems,$psPropset)    
  109.     foreach($Item in $fiItems.Items){      
  110.         $Headers = $null;  
  111.         $ClientSubmitTime = $null  
  112.         $DeliveryTime = $null  
  113.         [Void]$Item.TryGetProperty($PR_CLIENT_SUBMIT_TIME,[ref]$ClientSubmitTime)  
  114.         [Void]$Item.TryGetProperty($PR_MESSAGE_DELIVERY_TIME,[ref]$DeliveryTime)  
  115.         if($Item.TryGetProperty($PR_TRANSPORT_MESSAGE_HEADERS,[ref]$Headers)){  
  116.             $slen = $Headers.ToLower().IndexOf("`ndate: ")  
  117.             if($slen -gt 0){  
  118.                 $elen = $Headers.IndexOf("`r`n",$slen)  
  119.                 $TimeSpan =  NEW-TIMESPAN –Start $ClientSubmitTime –End $DeliveryTime   
  120.                 $rptobj = "" | select Subject,HeaderDate,DELIVERY_TIME,SUBMIT_TIME,Diff  
  121.                 $rptobj.Subject = $Item.Subject  
  122.                 $parsedDate = $Headers.Substring(($slen+7),($elen-($slen+7)))                 
  123.                 $rptobj.HeaderDate = [DateTime]::Parse($parsedDate).ToLocalTime()  
  124.                 $rptobj.DELIVERY_TIME = $DeliveryTime.ToLocalTime()  
  125.                 $rptobj.SUBMIT_TIME = $ClientSubmitTime.ToLocalTime()  
  126.                 $rptobj.Diff = [Math]::Round($TimeSpan.TotalMinutes,0)  
  127.                 $rptCollection += $rptobj  
  128.             }  
  129.         }        
  130.     }      
  131.     $ivItemView.Offset += $fiItems.Items.Count  
  132.     Write-Host ("Processed " + $ivItemView.Offset + " of " + $fiItems.TotalCount)  
  133. }while($fiItems.MoreAvailable -eq $true)   
  134. $rptCollection | Export-Csv -NoTypeInformation -Path "c:\Temp\$mailboxName-mTimes.csv"  
  135. Write-Host("Exported to c:\Temp\$mailboxName-mTimes.csv")  

Creating a Mailbox alias usage report with EWS and Powershell

$
0
0
Before starting this one I want to point out the method I'm going to demonstrate in this post is not the best method to use to do this. If you have Message Tracking available then you can do this much more accurately using the Message tracking logs.  But if you don't have this available to you or you just want to see what you can do with EWS then this is the post for you.

Many people have one or more email addresses assigned to their mailboxes (they get referred to using different names alias's, proxyaddress's, secondary address's, alternate address's etc) but in Outlook and OWA it can be hard to see what email address has been used to send you a mail because of the delivery process and the way the addresses are resolved. To get the actual email address a particular email was sent to one workaround you can use in EWS is to use the PidTagTransportMessageHeaders property and then parse the TO, CC and BCC Headers (note generally the BCC header won't be there even if you have been BCC'd) from the transport headers.

So what this script does is first it uses the Exchange Management Shell's get-mailbox cmdlet to grab all proxyaddresses for the target account. This is needed because there is noway in EWS to get all the Mailbox proxy addresses (you can use ResolveName to get the Mailbox Contact from AD but you will only get returned the first 3 addresses for the mailbox). It stores these addresses in a hashtable that can then be used in to do lookups so when processing all the emailaddress in header we can filter to only build a report of the current mailboxes addresses being used. The next part of the code is some normal EWS fair to grab all the messages in the Inbox for the last 7 days and then grab the PidTagTransportMessageHeaders  property and parse the TO,CC and BCC addresses from the headers using a linear parser and some RegEX (I know some people don't like linear parsing but I always find they work well).  The script then builds two reports the first report is a summary report showing each of the alias's that where used and how many TO,CC and BCC where used eg



The other report that the script builds is an Item Report which shows for each Item in the Inbox which method was used eg


Now as I mentioned there are a few problems with the method used in the script if you have been BCC'd on a message generally there won't be any information in the headers to reflect which email address was used. Also if the message was sent to a Distribution list or it's a piece of GrayMail depending on whatever method the mailer has used there maybe no header. So in this case it will show other in the method and the last parsed TO address.

To run this script you need to have a Remote Powershell session open so you can run Get-Mailbox and then just run the script with the PrimaryEmaillAddress of the mailbox you want to run the script against as a parameter. The script will output the two reports to the temp directory

I've put a download of this script here the code looks like

  1. ## Get the Mailbox to Access from the 1st commandline argument  
  2.   
  3. $MailboxName = $args[0]  
  4.   
  5. $Mailbox = get-mailbox $MailboxName  
  6. $mbAddress = @{}  
  7. foreach($emEmail in $Mailbox.EmailAddresses){  
  8.     if($emEmail.Tolower().Contains("smtp:")){  
  9.         $mbAddress.Add($emEmail.SubString(5).Tolower(),0)  
  10.     }  
  11. }  
  12.   
  13. ## Load Managed API dll    
  14.   
  15. ###CHECK FOR EWS MANAGED API, IF PRESENT IMPORT THE HIGHEST VERSION EWS DLL, ELSE EXIT  
  16. $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")  
  17. if (Test-Path $EWSDLL)  
  18.     {  
  19.     Import-Module $EWSDLL  
  20.     }  
  21. else  
  22.     {  
  23.     "$(get-date -format yyyyMMddHHmmss):"  
  24.     "This script requires the EWS Managed API 1.2 or later."  
  25.     "Please download and install the current version of the EWS Managed API from"  
  26.     "http://go.microsoft.com/fwlink/?LinkId=255472"  
  27.     ""  
  28.     "Exiting Script."  
  29.     exit  
  30.     }  
  31.     
  32. ## Set Exchange Version    
  33. $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2    
  34.     
  35. ## Create Exchange Service Object    
  36. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
  37.     
  38. ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials    
  39.     
  40. #Credentials Option 1 using UPN for the windows Account    
  41. $psCred = Get-Credential    
  42. $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())    
  43. $service.Credentials = $creds        
  44.     
  45. #Credentials Option 2    
  46. #service.UseDefaultCredentials = $true    
  47.     
  48. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  49.     
  50. ## Code From http://poshcode.org/624  
  51. ## Create a compilation environment  
  52. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  53. $Compiler=$Provider.CreateCompiler()  
  54. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  55. $Params.GenerateExecutable=$False  
  56. $Params.GenerateInMemory=$True  
  57. $Params.IncludeDebugInformation=$False  
  58. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  59.   
  60. $TASource=@' 
  61.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  62.     public class TrustAll : System.Net.ICertificatePolicy { 
  63.       public TrustAll() {  
  64.       } 
  65.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  66.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  67.         System.Net.WebRequest req, int problem) { 
  68.         return true; 
  69.       } 
  70.     } 
  71.   } 
  72. '@   
  73. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  74. $TAAssembly=$TAResults.CompiledAssembly  
  75.   
  76. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  77. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  78. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  79.   
  80. ## end code from http://poshcode.org/624  
  81.     
  82. ## 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    
  83.     
  84. #CAS URL Option 1 Autodiscover    
  85. $service.AutodiscoverUrl($MailboxName,{$true})    
  86. "Using CAS Server : " + $Service.url     
  87.      
  88. #CAS URL Option 2 Hardcoded    
  89.     
  90. #$uri=[system.URI] "https://casservername/ews/exchange.asmx"    
  91. #$service.Url = $uri      
  92.     
  93. ## Optional section for Exchange Impersonation    
  94.     
  95. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  96.   
  97. # Bind to the Inbox Folder  
  98. $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)     
  99. $Inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)  
  100. $PR_TRANSPORT_MESSAGE_HEADERS = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x007D,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);  
  101. $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly)    
  102. $psPropset.Add($PR_TRANSPORT_MESSAGE_HEADERS)  
  103. $psPropset.Add([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject)  
  104. $psPropset.Add([Microsoft.Exchange.WebServices.Data.ItemSchema]::Size)  
  105. $psPropset.Add([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived)  
  106. #Define ItemView to retrive just 1000 Items      
  107. $ivItemView =  New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)    
  108. $ivItemView.PropertySet = $psPropset  
  109. $rptCollection = @()  
  110. $rptcntCount = @{}  
  111. #Find Items that are unread  
  112. $sfItemSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThan([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived, (Get-Date).AddDays(-7));   
  113. $fiItems = $null      
  114. do{      
  115.     $fiItems = $service.FindItems($Inbox.Id,$sfItemSearchFilter,$ivItemView)      
  116.     [Void]$service.LoadPropertiesForItems($fiItems,$psPropset)    
  117.     foreach($Item in $fiItems.Items){      
  118.         $Headers = $null;  
  119.         $Address = "";  
  120.         $Method = ""  
  121.         if($Item.TryGetProperty($PR_TRANSPORT_MESSAGE_HEADERS,[ref]$Headers)){  
  122.             $slen = $Headers.ToLower().IndexOf("`nto: ")  
  123.             if($slen -gt 0){  
  124.                 $elen = $Headers.IndexOf("`r`n",$slen)                
  125.                 $ToAddressParsed = $Headers.SubString(($slen+4),$elen-($slen+4))  
  126.                 while($Headers.Substring($elen+2,1) -eq [char]9){  
  127.                     $slen = $elen+2  
  128.                     $elen = $Headers.IndexOf("`r`n",$slen)    
  129.                     $ToAddressParsed += $Headers.SubString(($slen),$elen-($slen))  
  130.                 }  
  131.                 $emailRegex = new-object System.Text.RegularExpressions.Regex("\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",[System.Text.RegularExpressions.RegexOptions]::IgnoreCase);  
  132.                 $emailMatches = $emailRegex.Matches($ToAddressParsed);  
  133.                 foreach($Match in $emailMatches){  
  134.                     if($mbAddress.Contains($Match.Value.Tolower())){  
  135.                         $Address = $Match  
  136.                         $Method = "To"  
  137.                         if($rptcntCount.ContainsKey($Match.Value)){  
  138.                             $rptcntCount[$Match.Value].To +=1  
  139.                         }  
  140.                         else{  
  141.                             $rptObj = "" | Select Address,To,CC,BCC  
  142.                             $rptObj.Address = $Match.Value  
  143.                             $rptObj.To = 1  
  144.                             $rptObj.CC = 0  
  145.                             $rptObj.BCC = 0  
  146.                             $rptcntCount.Add($Match.Value,$rptObj)                    
  147.                         }  
  148.                     }  
  149.                     else{  
  150.                         if($Address -eq ""){  
  151.                             $Address = $Match  
  152.                             $Method = "Other"  
  153.                         }  
  154.                     }  
  155.                 }  
  156.             }  
  157.             $slen = $Headers.ToLower().IndexOf("`ncc: ")  
  158.             if($slen -gt 0){  
  159.                 $elen = $Headers.IndexOf("`r`n",$slen)  
  160.                 $ccAddressParsed = $Headers.SubString(($slen+4),$elen-($slen+4))  
  161.                 while($Headers.Substring($elen+2,1) -eq [char]9){  
  162.                     $slen = $elen+2  
  163.                     $elen = $Headers.IndexOf("`r`n",$slen)    
  164.                     $ccAddressParsed += $Headers.SubString(($slen),$elen-($slen))  
  165.                 }  
  166.                 $emailRegex = new-object System.Text.RegularExpressions.Regex("\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",[System.Text.RegularExpressions.RegexOptions]::IgnoreCase);  
  167.                 $emailMatches = $emailRegex.Matches($ccAddressParsed);  
  168.                 foreach($Match in $emailMatches){  
  169.                     if($mbAddress.Contains($Match.Value.Tolower())){  
  170.                         $Address = $Match  
  171.                         $Method = "CC"  
  172.                         if($rptcntCount.ContainsKey($Match.Value)){  
  173.                             $rptcntCount[$Match.Value].CC +=1  
  174.                         }  
  175.                         else{  
  176.                             $rptObj = "" | Select Address,To,CC,BCC  
  177.                             $rptObj.Address = $Match.Value  
  178.                             $rptObj.To = 0  
  179.                             $rptObj.CC = 1  
  180.                             $rptObj.BCC = 0  
  181.                             $rptcntCount.Add($Match.Value,$rptObj)                    
  182.                         }  
  183.                     }  
  184.                 }  
  185.             }  
  186.             $slen = $Headers.ToLower().IndexOf("`nbcc: ")  
  187.             if($slen -gt 0){  
  188.                 $elen = $Headers.IndexOf("`r`n",$slen)  
  189.                 $bccAddressParsed = $Headers.SubString(($slen+4),$elen-($slen+4))  
  190.                 while($Headers.Substring($elen+2,1) -eq [char]9){  
  191.                     $slen = $elen+2  
  192.                     $elen = $Headers.IndexOf("`r`n",$slen)    
  193.                     $bccAddressParsed += $Headers.SubString(($slen),$elen-($slen))  
  194.                 }  
  195.                 $emailRegex = new-object System.Text.RegularExpressions.Regex("\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",[System.Text.RegularExpressions.RegexOptions]::IgnoreCase);  
  196.                 $emailMatches = $emailRegex.Matches($bccAddressParsed);  
  197.                 foreach($Match in $emailMatches){  
  198.                     if($mbAddress.Contains($Match.Value.Tolower())){  
  199.                         $Address = $Match  
  200.                         $Method = "BCC"  
  201.                         if($rptcntCount.ContainsKey($Match.Value)){  
  202.                             $rptcntCount[$Match.Value].BCC +=1  
  203.                         }  
  204.                         else{  
  205.                             $rptObj = "" | Select Address,To,CC,BCC  
  206.                             $rptObj.Address = $Match.Value  
  207.                             $rptObj.To = 0  
  208.                             $rptObj.CC = 0  
  209.                             $rptObj.BCC = 1  
  210.                             $rptcntCount.Add($Match.Value,$rptObj)                    
  211.                         }  
  212.                     }  
  213.                 }  
  214.             }  
  215.         }  
  216.         $ItemReport = "" | Select DateTimeReceived,Method,Address,Subject,Size  
  217.         $ItemReport.DateTimeReceived = $Item.DateTimeReceived  
  218.         $ItemReport.Subject = $Item.Subject  
  219.         $ItemReport.Size = $Item.Size  
  220.         $ItemReport.Method = $Method  
  221.         $ItemReport.Address = $Address  
  222.         $rptCollection += $ItemReport  
  223.           
  224.     }      
  225.     $ivItemView.Offset += $fiItems.Items.Count  
  226.     Write-Host ("Processed " + $ivItemView.Offset + " of " + $fiItems.TotalCount)  
  227. }while($fiItems.MoreAvailable -eq $true)   
  228. $rptcntCount.Values | ft  
  229. $rptcntCount.Values | Export-Csv -NoTypeInformation -path "c:\Temp\$MailboxName-MHSummaryReport.csv"  
  230. $rptCollection | Export-Csv -NoTypeInformation -path "c:\Temp\$MailboxName-MHItemReport.csv"  

Sending a No Reply, No ReplyAll, No Forward Email using EWS and Powershell

$
0
0
I've you haven't seen this before Gavin Smyth from Microsoft Research put together this cool VSTO plugin to allow you to send an Email from Outlook that will disable the Reply, ReplyAll and Forward Buttons on the Outlook ribbon http://research.microsoft.com/en-us/projects/researchdesktop/noreplyall.aspx and the how to posts about the VSTO http://blogs.msdn.com/b/gsmyth/archive/2011/09/18/noreply-vsto-add-in-wrap-up.aspx .

Note this only works in Outlook (not OWA or ActiveSync etc) but basically what a users would see when they receive a message is



How this works is it sets the PidLidVerbStream Property on a message which is mostly documented in the http://msdn.microsoft.com/en-us/library/ee218541(v=exchg.80).aspx protocol document. I say mostly because the format you get when using the methods from the NoReply Addin is a little different from the format documented which is for Voting buttons but its good enough to work with. The Verbs that this property refers to are the standard Verbs that MAPI implements which are documented in http://msdn.microsoft.com/en-us/library/office/cc815879(v=office.15).aspx .

So to use this same property in an EWS script you can just set it using the Extended Properties, the value you use is a little tricky as only the voteoption format is fully documented. But because this is pretty standard you can cut a past the Hex Values which contains the two streams from this property out of an existing message and then just change the bits that either enables or disable the Verbs for what you want to enable or disable on the Message your sending.

So this is what the following script does is allows you to send a message using EWS and set the options to enable and disable each of these verbs.

In the script I've got the following custom object where you can set the verbs on or off
  1. $VerbSetting = "" | Select DisableReplyAll,DisableReply,DisableForward,DisableReplyToFolder  
  2. $VerbSetting.DisableReplyAll = $true  
  3. $VerbSetting.DisableReply = $true  
  4. $VerbSetting.DisableForward = $true  
  5. $VerbSetting.DisableReplyToFolder = $true  

The rest of the script builds the VerbStream value property based on the setting in the custom object and the rest of the script is a standard EWS script to send a message.

I've put a download of this script here the script itself looks like

  1. $MailboxName = $args[0]  
  2. $SentTo = $args[1]  
  3.   
  4. $VerbSetting = "" | Select DisableReplyAll,DisableReply,DisableForward,DisableReplyToFolder  
  5. $VerbSetting.DisableReplyAll = $true  
  6. $VerbSetting.DisableReply = $true  
  7. $VerbSetting.DisableForward = $true  
  8. $VerbSetting.DisableReplyToFolder = $true  
  9.   
  10. ## Load Managed API dll    
  11.   
  12. ###CHECK FOR EWS MANAGED API, IF PRESENT IMPORT THE HIGHEST VERSION EWS DLL, ELSE EXIT  
  13. $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")  
  14. if (Test-Path $EWSDLL)  
  15.     {  
  16.     Import-Module $EWSDLL  
  17.     }  
  18. else  
  19.     {  
  20.     "$(get-date -format yyyyMMddHHmmss):"  
  21.     "This script requires the EWS Managed API 1.2 or later."  
  22.     "Please download and install the current version of the EWS Managed API from"  
  23.     "http://go.microsoft.com/fwlink/?LinkId=255472"  
  24.     ""  
  25.     "Exiting Script."  
  26.     exit  
  27.     }  
  28.     
  29. ## Set Exchange Version    
  30. $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2    
  31.     
  32. ## Create Exchange Service Object    
  33. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
  34.     
  35. ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials    
  36.     
  37. #Credentials Option 1 using UPN for the windows Account    
  38. $psCred = Get-Credential    
  39. $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())    
  40. $service.Credentials = $creds        
  41.     
  42. #Credentials Option 2    
  43. #service.UseDefaultCredentials = $true    
  44.     
  45. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  46.     
  47. ## Code From http://poshcode.org/624  
  48. ## Create a compilation environment  
  49. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  50. $Compiler=$Provider.CreateCompiler()  
  51. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  52. $Params.GenerateExecutable=$False  
  53. $Params.GenerateInMemory=$True  
  54. $Params.IncludeDebugInformation=$False  
  55. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  56.   
  57. $TASource=@' 
  58.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  59.     public class TrustAll : System.Net.ICertificatePolicy { 
  60.       public TrustAll() {  
  61.       } 
  62.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  63.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  64.         System.Net.WebRequest req, int problem) { 
  65.         return true; 
  66.       } 
  67.     } 
  68.   } 
  69. '@   
  70. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  71. $TAAssembly=$TAResults.CompiledAssembly  
  72.   
  73. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  74. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  75. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  76.   
  77. ## end code from http://poshcode.org/624  
  78.     
  79. ## 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    
  80.     
  81. #CAS URL Option 1 Autodiscover    
  82. $service.AutodiscoverUrl($MailboxName,{$true})    
  83. "Using CAS Server : " + $Service.url     
  84.      
  85. #CAS URL Option 2 Hardcoded    
  86.     
  87. #$uri=[system.URI] "https://casservername/ews/exchange.asmx"    
  88. #$service.Url = $uri      
  89.     
  90. ## Optional section for Exchange Impersonation    
  91.     
  92. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  93.   
  94.   
  95. function Get-VerbStream{    
  96.     param (    
  97.             $VerbSettings = "$( throw 'VerbSettings is a mandatory Parameter' )"    
  98.           )    
  99.     process{    
  100.       
  101.     $Header = "02010400000000000000"  
  102.     $ReplyToAllHeader = "055265706C790849504D2E4E6F7465074D657373616765025245050000000000000000"  
  103.     $ReplyToAllFooter = "0000000000000002000000660000000200000001000000"  
  104.     $ReplyToHeader = "0C5265706C7920746F20416C6C0849504D2E4E6F7465074D657373616765025245050000000000000000"  
  105.     $ReplyToFooter = "0000000000000002000000670000000300000002000000"  
  106.     $ForwardHeader = "07466F72776172640849504D2E4E6F7465074D657373616765024657050000000000000000"  
  107.     $ForwardFooter = "0000000000000002000000680000000400000003000000"  
  108.     $ReplyToFolderHeader = "0F5265706C7920746F20466F6C6465720849504D2E506F737404506F737400050000000000000000"  
  109.     $ReplyToFolderFooter = "00000000000000020000006C00000008000000"  
  110.     $VoteOptionExtras = "0401055200650070006C00790002520045000C5200650070006C007900200074006F00200041006C006C0002520045000746006F007200770061007200640002460057000F5200650070006C007900200074006F00200046006F006C0064006500720000"  
  111.     if($VerbSetting.DisableReplyAll){  
  112.         $DisableReplyAllVal = "00"  
  113.     }  
  114.     else{  
  115.         $DisableReplyAllVal = "01"  
  116.     }  
  117.     if($VerbSetting.DisableReply){  
  118.         $DisableReplyVal = "00"  
  119.     }  
  120.     else{  
  121.         $DisableReplyVal = "01"  
  122.     }  
  123.     if($VerbSetting.DisableForward){  
  124.         $DisableForwardVal = "00"  
  125.     }  
  126.     else{  
  127.         $DisableForwardVal = "01"  
  128.     }  
  129.     if($VerbSetting.DisableReplyToFolder){  
  130.         $DisableReplyToFolderVal = "00"  
  131.     }  
  132.     else{  
  133.         $DisableReplyToFolderVal = "01"  
  134.     }  
  135.     $VerbValue = $Header +  $ReplyToAllHeader + $DisableReplyAllVal + $ReplyToAllFooter + $ReplyToHeader + $DisableReplyVal +$ReplyToFooter + $ForwardHeader + $DisableForwardVal + $ForwardFooter + $ReplyToFolderHeader + $DisableReplyToFolderVal + $ReplyToFolderFooter + $VoteOptionExtras  
  136.     return $VerbValue  
  137.     }  
  138. }  
  139.   
  140. function hex2binarray($hexString){  
  141.     $i = 0  
  142.     [byte[]]$binarray = @()  
  143.     while($i -le $hexString.length - 2){  
  144.         $strHexBit = ($hexString.substring($i,2))  
  145.         $binarray += [byte]([Convert]::ToInt32($strHexBit,16))  
  146.         $i = $i + 2  
  147.     }  
  148.     return ,$binarray  
  149. }  
  150.   
  151.   
  152.   
  153. $VerbStreamProp = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,0x8520, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)  
  154.   
  155. $VerbSettingValue = get-VerbStream $VerbSetting  
  156.   
  157. $EmailMessage = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage -ArgumentList $service    
  158. $EmailMessage.Subject = "Message Subject"    
  159. #Add Recipients      
  160. $EmailMessage.ToRecipients.Add($SentTo)    
  161. $EmailMessage.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody    
  162. $EmailMessage.Body.BodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::HTML    
  163. $EmailMessage.Body.Text = "Body"   
  164. $EmailMessage.SetExtendedProperty($VerbStreamProp,(hex2binarray $VerbSettingValue))  
  165. $EmailMessage.SendAndSaveCopy()    
Viewing all 241 articles
Browse latest View live