MO_Module.psm1

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
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202

<#-----------------------------------------------------------------------------
PowerShell Module 'MO_Module'
 
Mike O'Neill, Microsoft Senior Premier Field Engineer (PFE)
https://mikeoneill.blog/
 
LEGAL DISCLAIMER
The sample scripts are not supported under any Microsoft standard support
program or service. The sample scripts are provided AS IS without warranty
of any kind. Microsoft further disclaims all implied warranties including,
without limitation, any implied warranties of merchantability or of fitness
for a particular purpose. The entire risk arising out of the use or
performance of the sample scripts and documentation remains with you. In no
event shall Microsoft, its authors, or anyone else involved in the creation,
production, or delivery of the scripts be liable for any damages whatsoever
(including, without limitation, damages for loss of business profits,
business interruption, loss of business information, or other pecuniary
loss) arising out of the use of or inability to use the sample scripts or
documentation, even if Microsoft has been advised of the possibility of such
damages.
-----------------------------------------------------------------------------#>


<#
.SYNOPSIS
    Compilation of scripts and functions to help administrate Exchange on premises, Exchange online, Exchange hybrid, and Active Directory environments.
.DESCRIPTION
    This is a compilation of scripts and functions packaged into a single module with verb-noun cmdlets for engineers to easily run and rerun as needed.
.NOTES
    Current Version : 1.2.5
 
    History:
            1.2.5 - Updated 8/27/2019
               - Added additional helpuri values to several functions
               - Corrected example information on several functions
               - Modified Connect-ExchangeOnline to be Connect-ExchangeOnlineNonMFA. Exchange PG is about to release Connect-ExchangeOnline in new REST API module with MFA available.
            1.2.4 - Updated 7/29/2019
               - Removed mandatory=$false from connection functions
               - added Get-MailboxLocations as a function
               - Added helpuri to functions. https://mikeoneill.blog/MO_Module is online location with sub folders for each function
               - Added 3rd example for connection functions
               - Updated help topics grammar in functions for greater clarity
            1.2.3 - Updated 6/26/2019
               - Fixed confirm information when connecting to on premises Exchange Server
               - Ensured functions worked on Windows Server 2019
               - Ensured functions worked aganist Exchange Server 2019
               - Corrected grammar for functions
               - Confirmed functions of working status and troubleshooting of processes
             
            1.2.2 - Updated 6/25/2019
               - Added parameter help content to functions
               - Fixed many PS Script Analyzer problems that were identified
               - Added in functions
                  - Update-SpecificDLCSVImport
                  - Update-SpecificDLTXTImport
                  - Get-EXOEmailSecuritySettings
                  - Move-MailQueDatabase
               - Changed prefix values in logon functions to be just -prefix. Should help make the parameter value easier to type and understand when using those functions.
               - Updated Get-MailboxAutoReplyConfigurationDomain function including comment block with setting a value to test in organization.
               - Added confirmation display when connecting to an on premises Exchange Server
     
            1.2 - Updated 6/16/2019
               - Updates Changed title of module to a more generic name: MO_Module
               - Adding both Exchange and Active Directory functions into one module
               - Some functions are generic for operating systems
               - Add Write-Verbose to functions missing the feature
 
            1.1 - Posted 7/7/2017
               - Fixes:
                  - removed many 'Global' scopes in parameters in functions
                  - fixed duplicate parameter entries in functions
 
            1.0 - Posted 5/21/2017 - First iteration
 
 
.FUNCTIONALITY
   This module assists Exchange and Active Directory engineers with a single point of reference with useful cmdlets in a PowerShell module.
#>


#region Connect to Exchange on-premises server

Function Request-CredentialExchangeOnPremises { # Request for new credentials function
<#
.SYNOPSIS
   Prompts user for a new user name and password for logon for onpremises server.
.DESCRIPTION
   This cmdlet prompts a user to enter in a new user name and password to correct any errors or change logons to a server on premises.
.EXAMPLE
   Request-CredentialOnPremises
    
   This function was built to re-enter on premises credentials so as not to have to close the PS session to flush the variable.
.INPUTS
    Requests users' credentials
.FUNCTIONALITY
   Prompts for new on premises credentials.
#>

[cmdletbinding()]
Param()
        &$Global:OnPremisesUserCredential
} # End Request for new onpremises credentials function

#region Enter Exchange onpremises Credentials to log onto onpremises server
     $Global:OnPremisesUserCredential = {
        $Global:OnPremisesCredential = Get-Credential -Message "Your Sign-In is your Domain\UserName."
}

#endregion Enter Credentials to log into on premises servers.

Function Connect-ExchangeServer {
<#
.SYNOPSIS
   Connects to a designated Exchange in the computer parameter
.DESCRIPTION
   Screen output of current status of DAG environment.
   Lists current Databases, what the status of each database is, and how many copies are available for each Database.
.EXAMPLE
   Connect-ExchangeServer -Computer MBX01
    
   To connect up to an Exchange server on named MBX01 and not use cmdlet prefix option.
.EXAMPLE
   Connect-ExchangeServer -Computer MBX04 -Prefix OnPrem
    
   This connects to Exchange server MBX04, and adds the prefix 'OnPrem' to all nouns in the remote PowerShell session.
.EXAMPLE
   Connect-ExchangeServer -Computer EX12 -Prefix OP
    
   This connects to Exchange server EX12, and adds the prefix 'OP' to all nouns in the remote PowerShell session.
.ROLE
   Exchange servers.
.FUNCTIONALITY
   Connects remotely to an on premises Exchange server.
#>

[cmdletbinding(HelpUri = 'https://mikeoneill.blog/MO_Module/Connect-ExchangeServer')]
    param ([parameter(Mandatory=$true,
         HelpMessage="Enter a single computer name to create a remote PowerShell session.")] 
         [string]$Computer,

         [parameter(HelpMessage="This is an optional parameter to add a prefix value to the Nouns in the remote PowerShell session to separate from other imported PowerShell sessions in the same PowerShell run space.")]
         [string]$Prefix = $Null
           )

If ($OnPremisesCredential -eq $null) {
    &$Global:OnPremisesUserCredential
}

If ($OnPremisesPrefix -eq $Null) {
        $Global:OnPremisesSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Computer/PowerShell/" -Authentication Kerberos -Credential $OnPremisesCredential
        Import-Module (Import-PSSession $OnPremisesSession -AllowClobber -DisableNameChecking ) -Global -DisableNameChecking
        Write-Verbose "`$Computer value is $Computer"
        $ConnectedExchangeServer = Get-ExchangeServer -Identity $Computer
        Write-Verbose ($ConnectedExchangeServer).name
         if ($ConnectedExchangeServer -ne $null) {
            Write-Host ""
            Write-Host "You are current logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Green
            Write-Host ""
         }
         else {
            Write-Host ""
            Write-Host "You are NOT current logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Red
            Write-Host ""
         }
    }
Else {
        $Global:OnPremisesSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Computer/PowerShell/" -Authentication Kerberos -Credential $OnPremisesCredential
        Import-Module (Import-PSSession $OnPremisesSession -AllowClobber -DisableNameChecking ) -Global -DisableNameChecking -Prefix $OnPremisesPrefix
        Write-Verbose "`$OnPremisesComputer value is $Computer"
        Write-Verbose "`$Prefix value is $Prefix"
        $ConnectedExchangeServer = Get-ExchangeServer -Identity $Computer
        Write-Verbose ($ConnectedExchangeServer).name
         if ($ConnectedExchangeServer -ne $null) {
            Write-Host ""
            Write-Host "You are currently logged onto the Exchange server: $ConnectedExchangeServer, with remote PS prefix of: $Prefix." -ForegroundColor Green
            Write-Host ""
         }
         else {
            Write-Host ""
            Write-Host "You are NOT currently logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Red
            Write-Host ""
         }
    }
}
#endregion Connect to Exchange on-premises server

#region Exchange Online sign in

Function Request-CredentialExchangeOnlineNonMFA {
<#
.SYNOPSIS
   Prompts user for a new user name and password for logon to tenant.
.DESCRIPTION
   This cmdlet prompts a user to enter in a new user name and password to correct any errors or change logons to an O365 tenant.
.EXAMPLE
   Request-CredentialExchangeOnlineNonMFA
 
   This function was built to re-enter O365 credentials so as not to have to close the PS session to flush the variable.
.FUNCTIONALITY
   Prompts for new O365 credentials that log onto Exchange online.
#>

[cmdletbinding()]
param()
        &$Global:EXOUserCredential
} # End Request for new credentials function

#region Enter Exchange Online Credentials to log onto tenant
     $Global:EXOUserCredential = {
        $Global:EXOCredential = Get-Credential -Message "Your Sign-In is your e-mail address for O365."
}

#endregion Enter Credentials to log onto tenant

#region Exchange Online connect/disconnect session functions

Function Connect-ExchangeOnlineNonMFA {
<#
.SYNOPSIS
   Creates a PSSession to connect to Exchange Online.
.DESCRIPTION
   This cmdlet combines the steps to successfully sign into an Exchange online tenant.
.EXAMPLE
   Connect-ExchangeOnlineNonMFA
    
   To connect up to an Exchange online tenant and not use cmdlet prefix option.
.EXAMPLE
   Connect-ExchangeOnlineNonMFA -prefix O365
    
   To connect up to an Exchange online tenant and use cmdlet prefix option, with 'O365' in this example.
.EXAMPLE
   Connect-ExchangeOnlineNonMFA -prefix EXO
    
   To connect up to an Exchange online tenant and use cmdlet prefix option, with 'EXO' in this example.
.FUNCTIONALITY
   Credentials are requested for an Exchange online tenant. A PSSession is then created using URL's to the Exchange online tenant, credentials are passed, remote module imported, and a confirmation that a connection is successful is displayed to the end user.
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/mo_module/connect-exchangeonlinenonmfa/')]
param ([parameter(HelpMessage="Optional prefix available to separate nouns for this PowerShell session from other imported PowerShell sessions in the same PowerShell run space.")]
      $Prefix = $Null)

If ($EXOCredential -eq $null)
{
    &$Global:EXOUserCredential
}
If ($Prefix -eq $Null) {

    $Global:EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/PowerShell/" -Credential $EXOCredential -Authentication basic -AllowRedirection
    Import-Module (Import-PSSession $Global:EXOSession -DisableNameChecking -AllowClobber) -Global -DisableNameChecking
    Connect-MsolService -Credential $Global:EXOCredential

    $myMBX = Get-MailBox $EXOCredential.UserName

    Write-Host ""
    Write-Host "Hello $($myMBX), you are now logged into Exchange Online." -ForegroundColor Green
    Write-Host ""
    }
else {

    $Global:EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/PowerShell/" -Credential $EXOCredential -Authentication basic -AllowRedirection
    Import-Module (Import-PSSession $Global:EXOSession -DisableNameChecking -AllowClobber) -Global -DisableNameChecking -Prefix $Prefix
    Connect-MsolService -Credential $Global:EXOCredential

    $myMBX = Get-MailBox $EXOCredential.UserName

    Write-Verbose "`$Prefix value is: $Prefix"

    Write-Host ""
    Write-Host "Hello $($myMBX), you are now logged into Exchange Online, using a noun prefix modifier of: $Prefix." -ForegroundColor Green
    Write-Host ""
    }
}

Function Disconnect-ExchangeOnlineNonMFA {
<#
.SYNOPSIS
   Disconnects a PSSession from Exchange Online.
.DESCRIPTION
   This cmdlet disconnects the sign-in session to an Exchange online tenant.
.EXAMPLE
   Disconnect-ExchangeOnlineNonMFA
 
   This function disconnects from the current Exchange online PowerShell session.
.FUNCTIONALITY
   Terminates the PSSession that is connected to an O365 Exchange online remote session and clears the credential variable.
#>


[cmdletbinding(HelpUri='https://mikeoneill.blog/mo_module/disconnect-exchangeonlinenonemfa/')]
param()
    Remove-PSSession $EXOSession
    $Credential = $null
}

#endregion Exchange Online connect/disconnect session functions

#endregion Exchange Online sign in

#region Test database status and present information of DB's: Get-DAGDatabaseInformation

Function Get-DAGDatabaseInformation { #look into trap to stop the error of not being connected to an Exchange server
<#
.SYNOPSIS
   This cmdlet reports on the current status of databases.
.DESCRIPTION
   Screen output of current status of databases in the Exchange environment.
   Lists current Databases, what the status of each database is, and how many copies are available for each Database.
.PARAMETER
    $Command = "Get-DAGDatabaseAvailabilityGroup"
.PARAMETER
    $OldPreference = $ErrorActionPreference
.PARAMETER
    $ErrorActionPreference = ‘stop’
.EXAMPLE
   Get-DatabaseInformation
.ROLE
   Exchange servers.
.FUNCTIONALITY
   Lists current Databases, what the status of each database is, and how many copies are available for each Database.
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/mo_module/get-dagdatabaseinformation/')]
 Param ($Command = "Get-DatabaseAvailabilityGroup")
      $OldPreference = $ErrorActionPreference
      $ErrorActionPreference = ‘stop’

try {
    if(Get-Command $command){

        if ($TestForDAG -eq $null) {
            "There is not a DAG in this Exchange environment."
        }
        Else {
            (Get-DatabaseAvailabilityGroup -Identity (Get-MailboxServer -Identity $Global:OnPremisesComputer).DatabaseAvailabilityGroup).Servers | Test-MapiConnectivity | Sort-Object Database | Format-Table -AutoSize
        }

        Get-MailboxDatabase | Sort-Object Name | Get-MailboxDatabaseCopyStatus | Format-Table -AutoSize
        function CopyCount {
            $DatabaseList = Get-MailboxDatabase | Sort-Object Name
            $DatabaseList | ForEach-Object {
                $Results = $_ | Get-MailboxDatabaseCopyStatus
                $Good = $Results | Where-Object { ($_.Status -eq "Mounted") -or ($_.Status -eq "Healthy") }
                $_ | Add-Member NoteProperty "CopiesTotal" $Results.Count
                $_ | Add-Member NoteProperty "CopiesFailed" ($Results.Count-$Good.Count)
            }
            $DatabaseList | Sort-Object copiesfailed -Descending | Format-Table name,copiesTotal,copiesFailed -AutoSize
        }
        CopyCount

        (Get-DatabaseAvailabilityGroup) | ForEach-Object {$_.Servers | ForEach-Object {Test-ReplicationHealth -Server $_}}
        }
    }

Catch {Write-Host "You need to connect to an Exchange Server first, before this function will process." -ForegroundColor Red}

Finally {$ErrorActionPreference=$oldPreference}

}

#endregion Test database status and present information of DB's: Get-DAGDatabaseInformation

#region Obtain .NET version currently installed on OS.

Function Get-DotNETVersion {
<#
.SYNOPSIS
   Displays .NET version on a computer.
.DESCRIPTION
   This cmdlet obtains the current version of .NET on either the local machine or a remote computer.
   Listed are the current version registry values and the related .NET version with the corresponding Exchange Server supported version.
.EXAMPLE
   Get-DotNETVersion
 
   Displays the current .NET version on the machine that this is run on.
.EXAMPLE
   Get-DotNETVersion -Computer ServerToTest
 
   This will display the .NET verion on the 'ServerToTest' in your environment.
.FUNCTIONALITY
   Displays the current release value and version of .NET.
#>


    [cmdletbinding(HelpUri = 'https://mikeoneill.blog/MO_Module/Get-DotNETVersion')]
    Param ([parameter(
         HelpMessage="Enter a computer name if needing to run this function against a remote machine.")]
         $Computer)

    If ($Computer -ne $null) {
    New-PSSession -ComputerName $Computer
}
        $NetValue = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name "Release" #Registry key value to test for version of .NET

<#Reference page of .NET values:
   Start-Process "https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx"
#>


    $NetValue | Select-Object Release, @{
        name=".NET Version / Supported Exchange Version(s)"
        expression={
      switch($_.Release) {
        378389 { " 4.5 / 2013 CU14" }
        378675 { " 4.5.1 / 2013 CU14, CU15" }
        378758 { " 4.5.1 / 2013 CU14, CU15" }
        379893 { " 4.5.2 / 2013 CU14, CU15 & 2016 CU3, CU4" }
        393295 { " 4.6 / None" }
        393297 { " 4.6 / None" }
        394254 { " 4.6.1 / 2013 CU14, CU15, CU16, & 2016 CU3, CU4" }
        394271 { " 4.6.1 / 2013 CU14, CU15, CU16, & 2016 CU3, CU4" }
        394802 { " 4.6.2 / 2013 CU16-CU20, & 2016 CU5-CU9" }
        394806 { " 4.6.2 / 2013 CU16-CU20, & 2016 CU5-CU9" }
        460798 { " 4.7 / None" }
        460805 { " 4.7 / None" }
        461308 { " 4.7.1 / 2013 CU19 and later, & 2016 CU9-CU12" }
        461310 { " 4.7.1 / 2013 CU19 and later, & 2016 CU9-CU12" }
        461808 { " 4.7.2 / 2013 CU21-CU25, & 2016 CU11-CU15, & 2019TRM-CU4" }
        461814 { " 4.7.2 / 2013 CU21-CU25, & 2016 CU11-CU15, & 2019TRM-CU4" }
        528040 { " 4.8 / 2013 CU22 and later, & 2016 CU13 and later, & 2019 CU2 and later" }
        528049 { " 4.8 / 2013 CU22 and later, & 2016 CU13 and later, & 2019 CU2 and later" }
        default {"Unknown version of .NET and unknown support of Exchange Server"}
      }
    }
}

}

#endregion Obtain .NET version currently installed on OS.

#region Start DAG Maintenance Mode

Function Start-DAGMaintenanceMode {
<#
.SYNOPSIS
   Function to put an Exchange 2013/2016 Server into Maintenance Mode.
 
   Credits:
   --------
   Exchange Server 2013 Maintenance Mode Script (Start):
   https://gallery.technet.microsoft.com/exchange/Exchange-Server-2013-ff6c942f
 
   Checking for admin credentials:
   http://blogs.technet.com/b/heyscriptingguy/archive/2011/05/11/check-for-admin-credentials-in-a-powershell-script.aspx
 
.DESCRIPTION
   This function is created to automatically put an Exchange 2010/2013/2016/2019 Server into Maintenance Mode.
   It will detect if the server is a Mailbox Server and then take appropriate additional actions, if any.
 
.EXAMPLE
   Running the following command will place a server called "Server1" into Maintenance Mode.
 
   Start-ExchangeServerMaintenanceMode -Server Server1
.EXAMPLE
   Running the following command will place a server called "Server1" into Maintenance Mode and move any messages in transit from that server to "Server2".
   Please note that the TargetServer value has to be a FQDN!
 
   Start-ExchangeServerMaintenanceMode -Server Server1 -TargetServerFQDN Server2.domain.com
#>


[CmdletBinding(HelpUri='https://mikeoneill.blog/mo_module/start-dagmaintenancemode/')]
Param
(
    # determine what server to put in maintenance mode
    [Parameter(Mandatory=$true,
               ValueFromPipelineByPropertyName=$true,
               Position=0)]
    [string]$Server,

    [Parameter(Mandatory=$false,
               ValueFromPipelineByPropertyName=$true,
               Position=1)]
    [string]$TargetServerFQDN = $Server
)

function evaluatequeues(){
    $MessageCount = Get-Queue -Server $Server | Where-Object {$_.Identity -notlike "*\Poison" -and $_.Identity -notlike"*\Shadow\*"} | Select-Object MessageCount
    $count = 0
    Foreach($message in $MessageCount){
        $count += $message.messageCount
    }
    if($count -ne 0){
        Write-Output "INFO: Sleeping for 30 seconds before checking the transport queues again..." -ForegroundColor Yellow
        Start-Sleep -s 30
        evaluatequeues
    }
    else{
        Write-Host "INFO: Transport queues are empty." -ForegroundColor Yellow
        Write-Host "INFO: Putting the entire server into maintenance mode..." -ForegroundColor Yellow
        if(Set-ServerComponentState $Server -Component ServerWideOffline -State Inactive -Requester Maintenance){
            Write-Host "INFO: Done! The components of $Server have successfully been placed into an inactive state!"
        }
        Write-Host "INFO: Restarting MSExchangeTransport service on server $Server..." -ForegroundColor Yellow
            #Restarting transport services based on info from http://blogs.technet.com/b/exchange/archive/2013/09/26/server-component-states-in-exchange-2013.aspx
            #Restarting the services will cause the transport services to immediately pick up the changed state rather than having to wait for a MA responder to take action
            Invoke-Command -ComputerName $Server {Restart-Service MSExchangeTransport | Out-Null}

        #restart Front End Transport Services if server is also CAS
        if($discoveredServer.IsFrontendTransportServer -eq $true){
            Write-Host "INFO: Restarting the MSExchangeFrontEndTransport Service on server $Server..." -ForegroundColor Yellow
            Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport} | Out-Null
        }
        Write-Host "`nINFO: Done! Server $Server is put succesfully into maintenance mode!`n" -ForegroundColor Green
    }

}

$discoveredServer = Get-ExchangeServer -Identity $Server | Select-Object IsHubTransportServer,IsFrontendTransportServer,AdminDisplayVersion

#Check for Administrative credentials
If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
    Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!"
    Break
}

    if($discoveredServer.IsHubTransportServer -eq $True){
        if(-NOT ($TargetServerFQDN)){
            Write-Warning "TargetServerFQDN is required."
            $TargetServerFQDN = Read-Host -Prompt "Please enter the TargetServerFQDN: "
        }

        #Get the FQDN of the Target Server through DNS, even if the input is just a host name
        try{
            $TargetServer = ([System.Net.Dns]::GetHostByName($TargetServerFQDN)).Hostname
        }
        catch{
            Write-Warning "Could not resolve ServerFQDN: $TargetServerFQDN";break
        }

        if((Get-ExchangeServer -Identity $TargetServer | Select-Object IsHubTransportServer).IsHubTransportServer -ne $True){
            Write-Warning "The target server is not a valid Mailbox server."
            Write-Warning "Aborting script..."
            Break
        }

        #Redirecting messages to target system
        Write-Host "INFO: Suspending Transport Service. Draining remaining messages..." -ForegroundColor Yellow
        Set-ServerComponentState $Server -Component HubTransport -State Draining -Requester Maintenance
        Redirect-Message -Server $Server -Target $TargetServer -Confirm:$false

        #suspending cluster node (if the server is part of a DAG)
        $mailboxserver = Get-MailboxServer -Identity $Server | Select-Object DatabaseAvailabilityGroup
        if($mailboxserver.DatabaseAvailabilityGroup -ne $null){
            Write-Host "INFO: Server $Server is a member of a Database Availability Group. Suspending the node now.`n" -ForegroundColor Yellow
            Write-Host "INFO: Node information:" -ForegroundColor Yellow
            Write-Host "-----------------------" -ForegroundColor Yellow
            Write-Host ""
            Write-Host ""
            Invoke-Command -ComputerName $Server -ArgumentList $Server {Suspend-ClusterNode $args[0]}
            Set-MailboxServer $Server -DatabaseCopyActivationDisabledAndMoveNow $true
            Set-MailboxServer $Server -DatabaseCopyAutoActivationPolicy Blocked
        }

        #Evaluate the Transport Queues and put into maintenance mode once all queues are empty
        evaluatequeues

    }
    else{
        Write-Host "INFO: Server $Server is a Client Access Server-only server." -ForegroundColor Yellow
        Write-Host "INFO: Putting the server components into inactive state" -ForegroundColor Yellow
        Set-ServerComponentState $Server -Component ServerWideOffline -State Inactive -Requester Maintenance
        Write-Host "INFO: Restarting transport services..." -ForegroundColor Yellow
        if(Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport | Out-Null}){
            Write-Host "INFO: Successfully restarted MSExchangeFrontEndTransport service" -ForegroundColor Yellow
        }
        Write-Host "`nINFO: Done! Server $Server is put succesfully into maintenance mode!`n" -ForegroundColor Green
    }
}

#endregion Start DAG Maintenance Mode

#region Stop DAG Maintenance Mode

Function Stop-DAGMaintenaceMode {
<#
.SYNOPSIS
   Function to take an Exchange 2013/2016 Server out of Maintenance Mode.
 
   Credits:
   --------
   Exchange Server 2013 Maintenance Mode Script (Stop):
   https://gallery.technet.microsoft.com/Exchange-Server-2013-77a71eb2
 
   Checking for admin credentials:
   http://blogs.technet.com/b/heyscriptingguy/archive/2011/05/11/check-for-admin-credentials-in-a-powershell-script.aspx
 
.DESCRIPTION
   This function is created to take an Exchange 2010/2013/2016/2019 Server out of Maintenance Mode.
   It will detect if the server is a Mailbox Server and then take appropriate additional actions, if any.
 
.EXAMPLE
   Running the following command will take a server called "Server1" out of Maintenance Mode:
 
   Stop-ExchangeServerMaintenanceMode -Server Server1
#>


[CmdletBinding(HelpUri='https://mikeoneill.blog/mo_module/stop-dagmaintenancemode/')]
Param
(
    # determine what server to put in maintenance mode
    [Parameter(Mandatory=$true,
               ValueFromPipelineByPropertyName=$true,
               Position=0)]
    [string]$Server
)

$discoveredServer = Get-ExchangeServer -Identity $Server | Select-Object IsHubTransportServer,IsFrontendTransportServer,AdminDisplayVersion

#Check for Administrative credentials
If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
    Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!"
    Break
}

Write-Host "INFO: Reactivating all server components..." -ForegroundColor Yellow
    Set-ServerComponentState $server -Component ServerWideOffline -State Active -Requester Maintenance
Write-Host "INFO: Server component states changed back into active state using requester 'Maintenance'" -ForegroundColor Yellow

if($discoveredServer.IsHubTransportServer -eq $true){

    $mailboxserver = Get-MailboxServer -Identity $Server | Select-Object DatabaseAvailabilityGroup

    if($mailboxserver.DatabaseAvailabilityGroup -ne $null){
        Write-Host "INFO: Server $server is a member of a Database Availability Group. Resuming the node now.`n" -ForegroundColor Yellow
        Write-Host "INFO: Node information:" -ForegroundColor Green
        Write-Host "-----------------------" -ForegroundColor Green
        Write-Host ""
        Invoke-Command -ComputerName $Server -ArgumentList $Server {Resume-ClusterNode $args[0]}
        Set-MailboxServer $Server -DatabaseCopyActivationDisabledAndMoveNow $false
        Set-MailboxServer $Server -DatabaseCopyAutoActivationPolicy Unrestricted
    }

    Write-Host "INFO: Resuming Transport Service..." -ForegroundColor Yellow
    Set-ServerComponentState –Identity $Server -Component HubTransport -State Active -Requester Maintenance

    Write-Host "INFO: Restarting the MSExchangeTransport Service on server $Server..." -ForegroundColor Yellow
    Invoke-Command -ComputerName $Server {Restart-Service MSExchangeTransport} | Out-Null

}

#restart Front End Transport Services
if($discoveredServer.IsFrontendTransportServer -eq $true){
    Write-Host "INFO: Restarting the MSExchangeFrontEndTransport Service on server $Server..." -ForegroundColor Yellow
    Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport} | Out-Null
}

Write-Host ""
Write-Host "INFO: Done! Server $server successfully taken out of Maintenance Mode." -ForegroundColor Green
Write-Host ""

$ComponentStates = (Get-ServerComponentstate $Server).LocalStates | Where-Object {$_.State -eq "InActive"}
if($ComponentStates){
    Write-Warning "There are still some components inactive on server $Server."
    Write-Warning "Some features might not work until all components are back in an Active state."
    Write-Warning "Check the information below to see what components are still in an inactive state and which requester put them in that state."
    $ComponentStates
    Clear-Variable ComponentStates
}

}

#endregion Stop DAG Maintenance Mode

#region Update a Specific Distribution List

Function Update-SpecificDLCSVImport {
<#
.SYNOPSIS
   Function to update a specific DL.
.DESCRIPTION
   Simple function to import a CSV file with PrimarySMTPAddress as a column into a specific Distribution Group.
   The cmdlet of Update-DistributionGroupMember replaces all addresses that are currently in the DL with the new list of members.
   https://technet.microsoft.com/en-us/library/dd335049(v=exchg.160).aspx
.EXAMPLE
   Update-SpecificDLCSVImport -ImportPath c:\temp\file.csv -GroupToUpdate MyGroup
 
   This updates the 'MyGroup' DL with the content listed in c:\temp\file.csv file.
.EXAMPLE
   Update-SpecificDLCSVImport -ImportPath c:\users.csv -GroupToUpdate SecurityGroup
 
   This updates the 'SecurityGroup' DL with the content listed in c:\users.csv file.
.INPUTS
   Import Path of CSV file that contains PrimarySMTPAddress as a column.
.OUTPUTS
   Displays current list of users in requested group.
#>

[cmdletbinding()]
param (
      [Parameter(Mandatory=$true,
      HelpMessage="Enter the import path of the file location, including the file name.")]
      $ImportPath,

      [Parameter(Mandatory=$true,
      HelpMessage="You must enter the name of the distribution group to update.")]
      $GroupToUpdate)

      # Import the members from the CSV
      $memberobjs = Import-Csv $ImportPath

      # Convert the member objects from the CSV into a property independent array
      # You can change PrimarySMTPAddress to be any property in the CSV that the Update-DistributionGroupmember cmdlet will take
      $memberobjs | ForEach-Object {[array]$newmembers = $newmembers + $_.PrimarySMTPAddress}

      # Update the DG membership
      Update-DistributionGroupmember -Identity $GroupToUpdate -members $newmembers -Confirm:$false

      # Output the new DG Membership
      Get-DistributionGroupMember -Identity $GroupToUpdate
}

Function Update-SpecificDLTXTImport {
<#
.SYNOPSIS
   Function to update a specific DL.
.DESCRIPTION
   Simple function to import a TXT file with PrimarySMTPAddress listings, no column header is needed.
   The cmdlet of Update-DistributionGroupMember replaces all addresses that are currently in the DL with the new list of members.
   https://technet.microsoft.com/en-us/library/dd335049(v=exchg.160).aspx
.EXAMPLE
   Update-SpecificDLTXTImport -ImportPath c:\temp\file.txt -GroupToUpdate MyGroup
 
   This updates the 'MyGroup' DL with the content listed in c:\temp\file.txt file.
.EXAMPLE
   Update-SpecificDLTXTImport -ImportPath c:\users.txt -GroupToUpdate SecurityGroup
 
   This updates the 'SecurityGroup' DL with the content listed in c:\users.txt file.
.INPUTS
   Import Path of TXT file that contains PrimarySMTPAddress as a list.
.OUTPUTS
   Displays current list of users in requested group.
#>

[cmdletbinding()]
param (
      [Parameter(Mandatory=$true,
      HelpMessage="Enter the import path of the file location, including the file name.")]
      $ImportPath,

      [Parameter(Mandatory=$true,
      HelpMessage="You must enter the name of the distribution group to update.")]
      $GroupToUpdate
   )

    # Create array of users via the Get-Content import cmdlet
    $newmembers = Get-Content $ImportPath

    # Update the DG membership
    Update-DistributionGroupmember -Identity $GroupToUpdate -Members $newmembers -Confirm:$false

    # Output the new DG Membership
    Get-DistributionGroupMember -Identity $GroupToUpdate
}

#endregion Update a Specific Distribution List

#region restart IIS on all Exchange Servers in organization

Function Restart-ExchangeIIS {
<#
.SYNOPSIS
   Restarts the IIS process on all Exchange servers in an Exchange organization.
.DESCRIPTION
   This function creates an array of all Exchange servers in an organization.
   Then it sends out an IIS restart command via PowerShell to all servers listed in the array.
.EXAMPLE
   Restart-ExchangeIIS
 
   Will request all Exchange servers with Get-ExchangeServer and restart IIS on the array built.
.ROLE
   Must already be logged onto an Exchange server PowerShell session for Get-ExchangeServer cmdlet to process.
#>


[cmdletbinding(HelpUri = 'https://mikeoneill.blog/MO_Module/Restart-ExchangeIIS')]
   param()
   $ExchangeServers = Get-ExchangeServer #Must be logged in remotely to Exchange server or running this via Exchange Management Shell

   Write-Verbose "`$ExchangeServers array contains: $ExchangeServers"

ForEach ($ExchangeServer in $ExchangeServers) {
      Write-Host "Restarting the w3svc service on $($ExchangeServer.name)" -ForegroundColor Yellow
      Invoke-Command -ComputerName $ExchangeServer.name {Restart-Service w3svc -force}
      $Service = Get-Service -Name w3svc -ComputerName $ExchangeServer.name
      Write-Host "$($ExchangeServer.name) web service is $($Service.status)" -ForegroundColor Green
      Write-Host ""
   }
}

#endregion restart IIS on all Exchange Servers in organization

#region restart IIS on specific servers

Function Restart-IISOnServers {
<#
.SYNOPSIS
   Restart IIS on servers.
.DESCRIPTION
   Primarily designed to be used on Exchange servers, if IIS needs a restart, this function can be used on any servers running IIS.
   Load the parameter with a sinlge server or multiple servers wanting to process, then the invoke-command creates a remote connection to restart the IIS service.
.EXAMPLE
   Restart-IISOnServers -Computer PC1,PC2,PC3,PC4
 
   This will restart IIS on the following computers: PC1, PC2, PC3, PC4
.EXAMPLE
   Restart-IISOnServers -Computer DC1
 
   This will restart IIS on a single computer: DC1
.INPUTS
   Enter a single name or multiple names separated by commas to restart IIS on servers.
#>


[cmdletbinding(HelpUri = 'https://mikeoneill.blog/MO_Module/Restart-IISOnServers')]
param ([Parameter(Mandatory = $true,
   ValueFromPipeline=$false,
   ValueFromPipelineByPropertyName=$true,
   HelpMessage="Enter a single server or multiple server names, separated by commas, to restart IIS on the target computer(s).")]
   $Computer)

#Get-content -path c:\temp\listOfServersToBounce.txt = $ExchangeServers line to fix.
Write-Verbose "`$Computer array contains: $Computer"
    ForEach  ($Comp in $Computer) 
      {
         Write-Host "Restarting the w3svc service on $Comp" -ForegroundColor Yellow
         Invoke-Command -ComputerName $Comp {Restart-Service w3svc -force}
         $Service = Get-Service -Name w3svc -ComputerName $Comp
         Write-Host "$Comp web service is $($Service.status)" -ForegroundColor Green
         Write-Host ""
      }

   $Computer = $null
}

#endregion restart IIS on specific servers

#region restart IIS AutoD app pool, less abrasive than restarting IIS on all Exchange servers

Function Restart-AutoDAppPool {
<#
.SYNOPSIS
   Restarts IIS AutoDiscover app pool on all Exchange servers in Organization.
.DESCRIPTION
   Obtains all Exchange servers in the organization and restarts the IIS AutoDiscover application pool on all servers.
.EXAMPLE
   Restart-AutoDAppPool
 
   Will request all Exchange servers with Get-ExchangeServer and restart AutoDiscover app pool on the array of servers built.
.NOTES
   This function allows repeatable use of restarting the IIS app pool for AutoDiscover on Exchange servers.
   This function will not restart the entire IIS service, just the specific AutoD app pool.
   Must be logged into an Exchange server remotely via PowerShell or Exchange Management Shell.
#>

   [cmdletbinding(helpuri='https://mikeoneill.blog/MO_Module/Restart-AutoDAppPool/',
                  SupportsShouldProcess=$true,ConfirmImpact='High')]
   Param()
      $ExchangeServers = Get-ExchangeServer #Must be logged in remotely to Exchange server or running this via Exchange Management Shell

Write-Verbose "`$ExchangeServers array contains: $ExchangeServers" 

    $ExchangeServers | ForEach-Object {
            if ($pscmdlet.ShouldProcess("$_", "Restarting AutoDiscover application pool"))
            {
               Write-Host "Recycling AutoDiscover application pool on $_" -ForegroundColor Yellow
               Invoke-Command -ComputerName $_.name -ScriptBlock { Get-ChildItem IIS:\AppPools | Where-Object { $_.name -like "*autodisc*" } | Restart-WebAppPool
            }
        }
    }
   $ExchangeServers = $null
}
#endregion restart IIS AutoD app pool, less abrasive than restarting IIS on all Exchange servers

#region Get: SPF, DMARC, DKIM from O365 tenant

Function Get-EXOEmailSecuritySettings {
<#
.SYNOPSIS
   Present the current Email Security Settings of an o365 tenant.
.DESCRIPTION
   Shows the: SPF, DKIM, and DMAC settings of a tenant.
   This function confirms the status of these settings in order to review if the different e-mail security settings have been enabled and/or are configured.
.EXAMPLE
   Get-EXOEmailSecuritySettings
 
   Gets the currently logged in tenant settings.
.EXAMPLE
   Get-EXOEmailSecuritySettings -AcceptedDomains contoso.com
 
   Gets the contoso.com in tenant settings.
.NOTES
   Creator: Matt Fields - PFE
   Updated: Mike O'Neill - PFE
 
   Version 1.0 - created code and functioned up for repeatable usage.
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/mo_module/get-exoemailsecuritysettings/')]
param($AcceptedDomains = $(Get-AcceptedDomain))

Write-Verbose "Accepted domains obtained: $($AcceptedDomains)"
ForEach ($Domain in $AcceptedDomains) {

#SPF Check
$SPF = Resolve-DnsName -Type TXT -Name $Domain -ErrorAction SilentlyContinue | Where-Object {$_.Strings -like "v=spf1*"}
Write-Verbose "Domain working on: $($domain)"
Write-Verbose "`$spf array contains: $spf"

If (!($SPF)) {
        Write-Host -ForegroundColor White -BackgroundColor Red "SPF has not been configured for $($Domain.DomainName)"
        Write-Host
    }

Else {
        $SPF | Format-Table -AutoSize
    }

#DKIM Check
$DKIM = Get-DKIMSigningConfig $Domain.DomainName
Write-Verbose "`$DKIM array contains: $DKIM"

IF ($DKIM.Enabled -eq 'True') {
    $DKIM = Resolve-DnsName -ErrorAction SilentlyContinue -Type CNAME -Name "selector1._domainkey.$($Domain.DomainName)"

    IF (!($DKIM)) {
            Write-Host -ForegroundColor White -BackgroundColor Red "DKIM is enabled, but DNS entries have not been created for $($Domain.DomainName)"
            Write-Host
        }

     }
Else {
        Write-Host -ForegroundColor White -BackgroundColor Red "DKIM is not enabled for $($Domain.DomainName)"
        Write-Host
     }

#DMARC Check
   $DMARC = Resolve-DnsName -ErrorAction SilentlyContinue -Type TXT -Name "_dmarc.$($Domain.DomainName)" | Where-Object {$_.Strings -like "v=DMARC1*"}
   Write-Verbose "`$DMARC array contains: $DMARC"

If (!($DMARC)) {
            Write-Host -ForegroundColor White -BackgroundColor Red "DMARC has not been configured for $($Domain.DomainName)"
            Write-Host
      }

Else {
         $DMARC | Format-Table -AutoSize
     }

    }
}

#endregion SPF, DMARC, DKIM from O365 tenant

#region Get domain specific OOF settings

Function Get-MailboxAutoReplyConfigurationDomain {
<#
.SYNOPSIS
   Lists OOF settings for users within the domain of specific SMTP addresses in a domain.
.DESCRIPTION
   Lists OOF settings if they are enabled and what values are set for users.
   This function only displays enabled OOF's and not disabled ones.
.EXAMPLE
   Get-MailboxAutoReployConfigurationDomain -Domain Contoso.com
 
   This lists end users with an @contoso.com SMTP address if they have OOF settings enabled.
.EXAMPLE
   Get-MailboxAutoReployConfigurationDomain -Domain contoso.onmicrosoft.com
 
   This lists end users with an @contoso.onmicrosoft.com SMTP address if they have OOF settings enabled.
.INPUTS
   Need to define the domain parameter.
.OUTPUTS
   Output to the screen of values that have OOF set.
 
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/mo_module/get-mailboxautoreplyconfigurationdomain/')]
param([Parameter(Mandatory=$true,
   HelpMessage="Enter the domain value to be selected for scanning OOF values.")]
   $Domain)

Write-Verbose "`$Domain array contains: $Domain"

$Mailboxes=Get-Mailbox -ResultSize Unlimited | Where-Object {$_.PrimarySmtpAddress -like "*$Domain"}
Write-Verbose "`$Mailboxes array contains: $Mailboxes"

ForEach ($Mailbox in $Mailboxes)
   {
      $Mailbox | Get-MailboxAutoReplyConfiguration | Where-Object { $_.AutoReplyState -ne "Disabled" } | Select-Object Identity,StartTime,EndTime,AutoReplyState
   }

<#Demo to set for testing, change UserName to a valid value in the organization:
 
Scheudle OOF for testing of user.
Set-MailboxAutoReplyConfiguration -Identity UserName -AutoReplyState Scheduled -StartTime "$(Get-Date).adddays(1)" -EndTime "$(Get-Date).addmonths(1)" -InternalMessage "Internal auto-reply message"
 
Disable OOF testing for user.
Set-MailboxAutoReplyConfiguration -Identity UserName -AutoReplyState disabled
#>


}

#endregion Get domain specific OOF settings

#region Get membership count of distribution and security groups

Function Get-GroupMemberCount {
<#
.SYNOPSIS
   Get member count of groups.
.DESCRIPTION
   Gets all distribution groups and security enabled groups and presents name and member count of each group.
   Outputs the content into a csv file.
.EXAMPLE
   Get-GroupMemberCount
 
   This will obtain distribution group list in CSV export. Default path is: $env:temp\DistributionGroups.csv
.EXAMPLE
   Get-GroupMemberCount -path c:\temp -FileName DGs.csv
 
   This will obtain distribution group list in CSV exported file to c:\temp titled DGs.csv
.OUTPUTS
   CSV file titled: SecurityGroups.csv in path parameter, default "$env:temp" which is: 'c:\Users\<UserName>\AppData\Local\Temp'
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/MO_Module/Get-GroupMemberCount/')]
param ($Path = $env:TEMP,
      $FileName = "DistributionGroups.csv")

$groups = @(Get-DistributionGroup -ResultSize unlimited) #Gets all distribution groups or e-mail enabled security groups.
Write-Verbose "`$groups array contains: $groups"

$report = @() #Creates empty array for report.

ForEach ($group in $groups)
    {
        $Count = @(Get-DistributionGroupMember -Identity $group.DistinguishedName).Count
        $ReportObject = New-Object PSObject
        $ReportObject | Add-Member NoteProperty -Name "GroupName" -Value $group.name
        $ReportObject | Add-Member NoteProperty -Name "Count" -value $Count
        $Report += $ReportObject
    }

$Report | Export-Csv -Path $Path\$FileName -NoTypeInformation

Write-Verbose "`$Groups array contains: $groups"
Write-Verbose "`$Path array contains: $Path"
Write-Verbose "`$FileName array contains: $FileName"

If (Test-Path -Path $Path\$FileName)
    {
      Write-Host "File $Path\$FileName exists." -ForegroundColor Green
    }

Else
    {
      Write-Host "File $myDir\$FileName was not created." -ForegroundColor Red
    }

}

#endregion Get membership count of distribution and security groups

#region Get Outlook bit value

Function Get-OutlookBitValue {
<#
.SYNOPSIS
   Get-OutlookBitValue
.DESCRIPTION
   Obtains Outlook bit version, 32 or 64, for a local Windows computer.
.EXAMPLE
   Get-OutlookBitValue
 
   Displays current version of Outlook if it is x86 (32bit) or x64 (64bit)
.OUTPUTS
   Outputs to screen
.NOTES
   Only currently works against a local machine. No remote parameter built in for function.
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/MO_Module/Get-OutlookBitValue')]
Param()

"WOW6432Node\","" | ForEach-Object { Get-ChildItem -Path "HKLM:\SOFTWARE\$($_)Microsoft\Office" } | Where-Object { (Get-ItemProperty -ea Ignore -Path "$($_.PsPath)\Outlook").Bitness } | Format-Table -AutoSize -Prop @{N="RegKey";E={$_.Name}},@{N="Bitness";E={(Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness}}

"WOW6432Node\","" | ForEach-Object { Get-ChildItem -Path "HKLM:\SOFTWARE\$($_)Microsoft\Office" } | Where-Object { $_.PSChildName -match "\d" } | Where-Object { Test-Path "$($_.PsPath)\Outlook" } | Where-Object { (Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness } | Format-Table -AutoSize -Prop @{N="RegKey";E={$_.Name}},@{N="Bitness";E={(Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness}}

[string[]]$outlookRegKeys = 'HKLM:\SOFTWARE\Microsoft\Office\14.0\Outlook',
    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\14.0\Outlook',
    'HKLM:\SOFTWARE\Microsoft\Office\15.0\Outlook',
    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\15.0\Outlook',
    'HKLM:\SOFTWARE\Microsoft\Office\16.0\Outlook',
    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\16.0\Outlook'

foreach ($outlookRegKey in $outlookRegKeys) {
    if (Test-Path $outlookRegKey) {
        $outlook = Get-ItemProperty -Path $outlookRegKey -Name 'Bitness'
        if ($outlook.Bitness -match '86') {
            Write-Host 'Outlook version is: x86 (32bit)' -ForegroundColor Green
        } else {
            Write-Host 'Outlook version is: x64 (64bit)' -ForegroundColor Green
        }
    }
}

}

#endregion Get Outlook bit value

#region Get-MailboxLocations

Function Get-MailboxLocations {
   <#
   .SYNOPSIS
      Displays locations of mailboxes in Exchange Online.
   .DESCRIPTION
      Determines the number of datacenters and locations where Exchange Online mailboxes are distributed.
   .EXAMPLE
      Get-MailboxLocations
 
      No parameters are needed.
   .OUTPUTS
      Displays content to screen.
   .NOTES
      Code used from Joe Palarchio from the Get-MailboxLocations.ps1 file. https://blogs.perficient.com/2016/03/15/office-365-script-to-determine-exchange-online-mailbox-location/
      Limitations: Table of datacenters is static and may need to be expanded as Microsoft brings additional datacenters online.
                       This function runs 'Get-Mailbox' which can take a really long time in EXO if the tenant has lots of mailboxes.
    
       This is not the 'Get-MailboxLocation' cmdlet: https://docs.microsoft.com/en-us/powershell/module/exchange/mailboxes/get-mailboxlocation?view=exchange-ps
   .COMPONENT
      Remote PowerShell connection to Exchange Online is required.
   #>

    
[cmdletbinding(HelpUri='https://mikeoneill.blog/MO_Module/Get-MailboxLocations')]
param()

     $Datacenter = @{}
     $Datacenter["CP"]=@("LAM","Brazil")
     $Datacenter["GR"]=@("LAM","Brazil")
     $Datacenter["HK"]=@("APC","Hong Kong")
     $Datacenter["SI"]=@("APC","Singapore")
     $Datacenter["SG"]=@("APC","Singapore")
     $Datacenter["KA"]=@("JPN","Japan")
     $Datacenter["OS"]=@("JPN","Japan")
     $Datacenter["TY"]=@("JPN","Japan")
     $Datacenter["AM"]=@("EUR","Amsterdam, Netherlands")
     $Datacenter["DB"]=@("EUR","Dublin, Ireland")
     $Datacenter["HE"]=@("EUR","Finland")
     $Datacenter["VI"]=@("EUR","Austria")
     $Datacenter["BL"]=@("NAM","Virginia, USA")
     $Datacenter["SN"]=@("NAM","San Antonio, Texas, USA")
     $Datacenter["BN"]=@("NAM","Virginia, USA")
     $Datacenter["DM"]=@("NAM","Des Moines, Iowa, USA")
     $Datacenter["BY"]=@("NAM","San Francisco, California, USA")
     $Datacenter["CY"]=@("NAM","Cheyenne, Wyoming, USA")
     $Datacenter["CO"]=@("NAM","Quincy, Washington, USA")
     $Datacenter["MW"]=@("NAM","Quincy, Washington, USA")
     $Datacenter["CH"]=@("NAM","Chicago, Illinois, USA")
     $Datacenter["ME"]=@("APC","Melbourne, Victoria, Australia")
     $Datacenter["SY"]=@("APC","Sydney, New South Wales, Australia")
     $Datacenter["KL"]=@("APC","Kuala Lumpur, Malaysia")
     $Datacenter["PS"]=@("APC","Busan, South Korea")
     $Datacenter["YQ"]=@("CAN","Quebec City, Canada")
     $Datacenter["YT"]=@("CAN","Toronto, Canada")
   
     Write-Host
     Write-Host "Gathering mailbox information..." -ForegroundColor Yellow
   
     $Mailboxes = Get-Mailbox -ResultSize Unlimited | Where-Object {$_.RecipientTypeDetails -ne "DiscoveryMailbox"}
   
     $ServerCount = ($Mailboxes | Group-Object {$_.ServerName}).count
   
     $Mailboxes = $Mailboxes | Group-Object {$_.ServerName.SubString(0,2)} | Select-Object @{Name="Datacenter";Expression={$_.Name}}, Count
   
     $Locations=@()
   
     # Not pretty error handling but allows counts to add properly when a datacenter location could not be identified from the table
     $E = $ErrorActionPreference
     $ErrorActionPreference = "SilentlyContinue"
   
     ForEach ($Mailbox in $Mailboxes) {
       $Object = New-Object -TypeName PSObject
       $Object | Add-Member -Name 'Datacenter' -MemberType NoteProperty -Value $Mailbox.Datacenter
       $Object | Add-Member -Name 'Region' -MemberType NoteProperty -Value $Datacenter[$Mailbox.Datacenter][0]
       $Object | Add-Member -Name 'Location' -MemberType NoteProperty -Value $Datacenter[$Mailbox.Datacenter][1]
       $Object | Add-Member -Name 'Count' -MemberType NoteProperty -Value $Mailbox.Count
       $Locations += $Object
     }
   
     $ErrorActionPreference = $E
   
     $TotalMailboxes = ($Locations | Measure-Object Count -Sum).sum
   
     $LocationsConsolidated = $Locations | Group-Object Location | ForEach-Object {
       New-Object PSObject -Property @{
       Location = $_.Name
       Mailboxes = ($_.Group | Measure-Object Count -Sum).Sum
       }
     } | Sort-Object Count -Descending
   
     Write-Host
     Write-Host -NoNewline "Your "
     Write-Host -NoNewline -ForegroundColor Yellow $TotalMailboxes
     Write-Host -NoNewline " mailboxes are spread across "
     Write-Host -NoNewline -ForegroundColor Yellow $ServerCount
     Write-Host -NoNewline " servers in "
     Write-Host -NoNewline -ForegroundColor Yellow $Locations.Count
     Write-Host -NoNewline " datacenters in "
     Write-Host -NoNewline -ForegroundColor Yellow $LocationsConsolidated.Count
     Write-Host " geographical locations."
     Write-Host
     Write-Host "The distribution of mailboxes is shown below:"
   
     $LocationsConsolidated | Select-Object Location, Mailboxes
     }
   
#endregion Get-MailboxLocations