Mar 03

Determine If Distribution Group is Being Used in 365 Exchange

“What distribution groups are in use?” and “How many emails are sent to a specific distribution group per month?” are common questions I receive with 365 Exchange or Exchange.  Unfortunately, there is nothing built in that tracks how many emails on sent to a distribution group.  However we can use Get-MessageTrace to count the number of messages sent to a distribution group for a time range with the max being 30 days.  Also note, the by default PageSize returns 1000 items but you can increase the PageSize to 5000 items.  For example, to get the number of emails sent to the distribution group everyone@domain.com for a single day we can use:

$DGCount = Get-MessageTrace -PageSize 5000 -RecipientAddress "everyone@domain.com" -StartDate ([DateTime]::Today.AddDays(-1)) -EndDate ([DateTime]::Today) | ForEach-Object {$count++}
$DGCount

Using this method, we can count the number of emails sent to each distribution group each day and store the results in an output file.  We can then query those output files and create a report.  In my example, the report will show the total emails sent to each distribution group by month and go back 12 months.  Now without further ado, let’s get to the two scripts needed.

365_DGCounter.ps1

#365_DGCounter.ps1
# ------ SCRIPT CONFIGURATION ------
#Log File
$LogFile = $MyInvocation.MyCommand.Path.Replace($MyInvocation.MyCommand.Name,"Logs\") + ((Get-Date).AddDays(-1).ToString('yyyy_MM_dd')) + ".xml"
#Table Name
$TableName = "DG_Emails_Received"
# ------ END CONFIGURATION ------

#Confirm Logs Directory Exists
$LogPath = $MyInvocation.MyCommand.Path.Replace($MyInvocation.MyCommand.Name,"Logs\")
If (!(Test-Path $LogPath)){
	New-Item -ItemType Directory -Force -Path $LogPath
}

#Import Modules and Connect to Office 365
import-module ActiveDirectory
Try {
	$UserCredential = Get-Credential
	$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $UserCredential -Authentication Basic -AllowRedirection
	Import-PSSession $Session
}
Catch {
	#Error Connecting to Office365
}

#Create Data Table
$CounterTable = New-Object system.Data.DataTable $TableName
$CounterCol1 = New-Object system.Data.DataColumn Date,([datetime])
$CounterCol2 = New-Object system.Data.DataColumn Email,([string])
$CounterCol3 = New-Object system.Data.DataColumn Count,([int])
$CounterTable.columns.add($CounterCol1)
$CounterTable.columns.add($CounterCol2)
$CounterTable.columns.add($CounterCol3)

#Get DG's and Add to Table
$DG = Get-DistributionGroup -Resultsize Unlimited | select Displayname, Primarysmtpaddress
$DG | ForEach-Object {
	$count = 0
	#Note: [DateTime]::Today returns 12:00 AM of the Current Date 
	#      Let's assume the date is currently 3/3/2016, the below returns the range 3/2/2016 12:00 AM to 3/3/2016 12:00 AM
	Get-MessageTrace -PageSize 5000 -RecipientAddress $_.Primarysmtpaddress -StartDate ([DateTime]::Today.AddDays(-1)) -EndDate ([DateTime]::Today) | ForEach-Object {$count++}
	$CounterRow = $CounterTable.NewRow()
	$CounterRow.Date = [datetime](Get-Date).AddDays(-1)
	$CounterRow.Email = $_.Primarysmtpaddress
	$CounterRow.Count = $count
	$CounterTable.Rows.Add($CounterRow)
}

#Output Results
$CounterTable.WriteXml($LogFile)
$CounterTable.WriteXmlSchema($LogFile.replace(".xml",".xsd"))
$CounterTable | format-table -AutoSize

#Disconnect From Office365
Remove-PSSession $Session

Note: [DateTime]::Today returns 12:00 AM of the Current Date.  Let’s assume the date is currently 3/3/2016.  The script above would return the range 3/2/2016 12:00 AM to 3/3/2016 12:00 AM.  Running this script will actually return the email counts from yesterday since today has not ended.

365_DGCounterReport.ps1

#365_DGCounterReport.ps1
# ------ SCRIPT CONFIGURATION ------
#Log File Path
$LogPath = $MyInvocation.MyCommand.Path.Replace($MyInvocation.MyCommand.Name,"Logs\")
#Table Names
$MasterTableName = "DG_Emails_Received"
$ResultTableName = "DG_Emails_Received"
# ------ END CONFIGURATION ------

#Import XML Files to Master Table
$MasterTable = New-Object system.Data.DataTable $MasterTableName
$MasterCol1 = New-Object system.Data.DataColumn Date,([datetime])
$MasterCol2 = New-Object system.Data.DataColumn Email,([string])
$MasterCol3 = New-Object system.Data.DataColumn Count,([int])
$MasterTable.columns.add($MasterCol1)
$MasterTable.columns.add($MasterCol2)
$MasterTable.columns.add($MasterCol3)
Get-ChildItem $LogPath -Filter "*.xml" | Sort-Object Name | ForEach-Object {
	$XmlDocument = [XML](Get-Content -Path $_.FullName)
	$XmlDocument.DocumentElement.$MasterTableName | ForEach-Object {
		$MasterRow = $MasterTable.NewRow()
		$MasterRow.Date = [datetime]$_.Date
		$MasterRow.Email = $_.Email
		$MasterRow.Count = [int]$_.Count
		$MasterTable.Rows.Add($MasterRow)
	}
}

#Get List of Unique Distribution Groups from Master Table
$DGs = @{}
$MasterTable | ForEach-Object {
	If (!($DGs.ContainsKey($_.Email))){
		$DGs.Add($_.Email,$_.Email)
	}
}
$DGs = $DGs.GetEnumerator() | Sort-Object Name

#Count Emails By Month
$ResultTable = New-Object system.Data.DataTable $ResultTableName
$ResultCol1 = New-Object system.Data.DataColumn Email,([string])
$ResultCol2 = New-Object system.Data.DataColumn Mon,([int])
$ResultCol3 = New-Object system.Data.DataColumn Mon_1,([int])
$ResultCol4 = New-Object system.Data.DataColumn Mon_2,([int])
$ResultCol5 = New-Object system.Data.DataColumn Mon_3,([int])
$ResultCol6 = New-Object system.Data.DataColumn Mon_4,([int])
$ResultCol7 = New-Object system.Data.DataColumn Mon_5,([int])
$ResultCol8 = New-Object system.Data.DataColumn Mon_6,([int])
$ResultCol9 = New-Object system.Data.DataColumn Mon_7,([int])
$ResultCol10 = New-Object system.Data.DataColumn Mon_8,([int])
$ResultCol11 = New-Object system.Data.DataColumn Mon_9,([int])
$ResultCol12 = New-Object system.Data.DataColumn Mon_10,([int])
$ResultCol13 = New-Object system.Data.DataColumn Mon_11,([int])
$ResultCol14 = New-Object system.Data.DataColumn Total,([int])
$ResultTable.columns.add($ResultCol1)
$ResultTable.columns.add($ResultCol2)
$ResultTable.columns.add($ResultCol3)
$ResultTable.columns.add($ResultCol4)
$ResultTable.columns.add($ResultCol5)
$ResultTable.columns.add($ResultCol6)
$ResultTable.columns.add($ResultCol7)
$ResultTable.columns.add($ResultCol8)
$ResultTable.columns.add($ResultCol9)
$ResultTable.columns.add($ResultCol10)
$ResultTable.columns.add($ResultCol11)
$ResultTable.columns.add($ResultCol12)
$ResultTable.columns.add($ResultCol13)
$ResultTable.columns.add($ResultCol14)
$CurDate = Get-Date
$DGs | ForEach-Object{
	$DG = $_.Name
	$ResultRow = $ResultTable.NewRow()
	$ResultRow.Email = $DG
	$ResultRow.Mon = 0
	$ResultRow.Mon_1 = 0
	$ResultRow.Mon_2 = 0
	$ResultRow.Mon_3 = 0
	$ResultRow.Mon_4 = 0
	$ResultRow.Mon_5 = 0
	$ResultRow.Mon_6 = 0
	$ResultRow.Mon_7 = 0
	$ResultRow.Mon_8 = 0
	$ResultRow.Mon_9 = 0
	$ResultRow.Mon_10 = 0
	$ResultRow.Mon_11 = 0
	$ResultRow.Total = 0
	$MasterTable | ForEach-Object {
		If ($DG -eq $_.Email){
			#Current Month
			If ($_.Date.Month -eq $CurDate.Month){
				$ResultRow.Mon += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-1
			If ($_.Date.Month -eq $CurDate.AddMonths(-1).Month){
				$ResultRow.Mon_1 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-2
			If ($_.Date.Month -eq $CurDate.AddMonths(-2).Month){
				$ResultRow.Mon_2 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-3
			If ($_.Date.Month -eq $CurDate.AddMonths(-3).Month){
				$ResultRow.Mon_3 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-4
			If ($_.Date.Month -eq $CurDate.AddMonths(-4).Month){
				$ResultRow.Mon_4 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-5
			If ($_.Date.Month -eq $CurDate.AddMonths(-5).Month){
				$ResultRow.Mon_5 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-6
			If ($_.Date.Month -eq $CurDate.AddMonths(-6).Month){
				$ResultRow.Mon_6 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-7
			If ($_.Date.Month -eq $CurDate.AddMonths(-7).Month){
				$ResultRow.Mon_7 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-8
			If ($_.Date.Month -eq $CurDate.AddMonths(-8).Month){
				$ResultRow.Mon_8 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-9
			If ($_.Date.Month -eq $CurDate.AddMonths(-9).Month){
				$ResultRow.Mon_9 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-10
			If ($_.Date.Month -eq $CurDate.AddMonths(-10).Month){
				$ResultRow.Mon_10 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-11
			If ($_.Date.Month -eq $CurDate.AddMonths(-11).Month){
				$ResultRow.Mon_11 += $_.Count
				$ResultRow.Total += $_.Count
			}
		}
	}
	$ResultTable.Rows.Add($ResultRow)
}

#Rewrite Column Names as Months
For ($i=0; $i -lt 12; $i++) {
	If ($i -eq 0){
		$ResultTable.columns["Mon"].ColumnName = "$((Get-Culture).DateTimeFormat.GetAbbreviatedMonthName($CurDate.Month)) $($CurDate.Year)"
	}Else{
		$ResultTable.columns["Mon_$($i)"].ColumnName = "$((Get-Culture).DateTimeFormat.GetAbbreviatedMonthName($CurDate.AddMonths($i * -1).Month)) $($CurDate.AddMonths($i * -1).Year)"
	}
}

#Output Results
$ResultTable | format-table -AutoSize
$ResultTable | Export-CSV Report.csv -notypeinformation
Apr 08

Matching Credit Card Numbers

Overview

Using regular expressions, you can easily match a credit card number.  You may wish to validate legit CC numbers, block financial information in emails, or audit security by finding financial information in documents.  There is no perfect algorithm or regex for detecting potential CCN’s, and there will always be false positives, etc.  Although regular expressions can match a CCN, they cannot confirm incorrect digits.  If you require a more robust solution, you will need to also implement the Luhn algorithm.  Moving forward, I’ll focus on detecting CCN’s in documents or emails.

Credit Card Info

The first 6 digits of a CCN are known as the Issuer Identification Number (IIN), which are used to identify the card issuer; the remaining digits can vary in length and are up to the issuer. Using the IIN and the CCN pre-defined length, we can identify blocks of numbers that belong to each issuer.  The credit card numbers are typically grouped with spaces or dashes in order to make them more readable.  We will need to keep this in mind when matching CCN’s.  I have listed US issuers, their IIN, and the CCN lengths below. For a full list including international issuers see http://en.wikipedia.org/wiki/Bank_card_number.

ISSUER IIN STARTING PATTERN LENGTH
American Express 34, 37 15
Diners Club 300-305, 309, 36, 38-39 15
Discover 6011, 622126-622925, 644-649, 65 16
JCB 3528-3589 16
MasterCard 50-55 16
Visa 4 16 or 13 on old cards

The Basics Validating Credit Cards

If we wanted to validate a credit card, you would first want to remove any spaces or dashes from the input.  Once the input is clean we can use a typical regular expression to match potential valid CCN’s. The below regex’s were originally taken from http://www.regular-expressions.info/creditcard.html, but I’ve updated the out of date expressions in accordance to the latest IIN changes as per the Wikipedia article listed above:

American Express

^3[47][0-9]{13}$

 

Diners Club

^3(?:0[0-5]|09|[68][0-9])[0-9]{11}$

 

Discover

^6(?:011|22[0-9]|5[0-9]{2})[0-9]{12}$

 

JCB

^35(?:2[89]|[3-8][0-9])[0-9]{12}$

 

Mastercard

^5[0-5][0-9]{14}$

 

Visa

^4[0-9]{12}(?:[0-9]{3})?$

 

However, if you are unable to strip the spaces and dashes out prior to validating the CCN, you’ll quickly find many shortcomings.  The above regex’s will not account for spaces or dashes as printed on the front of the card and will only detect a CCN when it’s the only thing on a line.  Obviously, this will not give us the results we desire for finding CCN’s in a document or email.  Instead, we will want to use \b to match on a word boundary instead of the carrot(^) and the dollar($).  In addition, we also want to add exceptions before and after the CCN we’re checking, which will help reduce false positives; this will allow us to eliminate items such as hyperlinks, order #’s, etc.  We can now use the below regex to surround each CC issuer’s rules that we want to detect:

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)CCN(?!\/)\b

 

  • Starting Position is a word boundary
  • Previous Character is not:
    • period(.)
    • left angle bracket(<)
    • right angle bracket(>)
    • dash()
    • plus(+)
    • forward slash(/) – Ignore false positives like http://www.domain.com/################/
    • Open parenthesis – Ignore false positives like (################)
    • Equal (=) – Ignores false positives like http://www.domain.com?si=################
    • Pound, Colon, Space(#:  ) – Ignore false positives like Order#: ################
    • dash, space(– )
    • ID, colon, space(ID: )
  • CCN – Represents the regex used to represent each issuers credit card number
  • Next character is not:
    • forward slash(/) – Ignore false positives like http://www.domain.com/################/
  • Ending position is a word boundary

Matching Credit Cards in a Document or Email

By doing a slight re-write of CC regex’s and combining it with our above wrapper, we can easily detect a CCN’s in a document or email. However, the oneliner for Discover CCN’s is quite long, and some systems limit the length of regex’s. Due to this, I’ve provided the oneliner plus shorter versions split up by the IIN.

American Express

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)3[47](?:\d{13}|\d{2}\s\d{6}\s\d{5}|\d{2}\-\d{6}\-\d{5})(?!\/)\b

 

Diners Club

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)3(?:0[0-5]|09|[689]\d)\d(?:\d{10}|\s\d{6}\s\d{4}|\-\d{6}\-\d{4})(?!\/)\b

 

Discover (Oneliner)

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)(?:6011\d{12}|6011(\s\d{4}){3}|6011(\-\d{4}){3}|64[4-9]\d{13}|64[4-9]\d(\s\d{4}){3}|64[4-9]\d(\-\d{4}){3}|65\d{14}|65\d{2}(\s\d{4}){3}|65\d{2}(\-\d{4}){3}|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5])\d{10}|622(1\s2[6-9]|1\s[3-9][0-9]|[2-8]\s[0-9]{2}|9\s[0-1][0-9]|9\s2[0-5])\d{2}(\s\d{4}){2}|622(1\-2[6-9]|1\-[3-9][0-9]|[2-8]\-[0-9]{2}|9\-[0-1][0-9]|9\-2[0-5])\d{2}(\-\d{4}){2})(?!\/)\b

 

Discover (6011,644-649,65 IIN’s)

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)6(?:011|4[4-9]\d|5\d{2})(?:\d{12}|(\s\d{4}){3}|(\-\d{4}){3})(?!\/)\b

 

Discover (622 IIN No Delimiter)

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5])\d{10}(?!\/)\b

 

Discover (622 IIN Space Delimiter)

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s)622(1\s2[6-9]|1\s[3-9][0-9]|[2-8]\s[0-9]{2}|9\s[0-1][0-9]|9\s2[0-5])\d{2}(\s\d{4}){2}(?!\/)\b

Note: Office 365 has a 128 character limit for the regex expression.  I have modified the default wrapper by removing a few items to keep this at 128 characters

 

Discover (622 IIN Dash Delimiter)

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s)622(1\-2[6-9]|1\-[3-9][0-9]|[2-8]\-[0-9]{2}|9\-[0-1][0-9]|9\-2[0-5])\d{2}(\-\d{4}){2}(?!\/)\b

Note: Office 365 has a 128 character limit for the regex expression.  I have modified the default wrapper by removing a few items to keep this at 128 characters

 

JCB

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)35(?:2[89]|[3-8]\d)(?:\d{12}|(\s\d{4}){3}|(\-\d{4}){3})(?!\/)\b

 

MasterCard

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)5[0-5](?:\d{14}|\d{2}(\s\d{4}){3}|\d{2}(\-\d{4}){3})(?!\/)\b

 

Visa

\b(?<![\.\<\>\-\+\/\(\=]|#:\s|\-\s|ID:\s)4(?:\d{11}|\d{3}\s(\d{4}\s){2}|\d{3}\-(\d{4}\-){2})\d(\d{3})?(?!\/)\b

If we test the above regex in an online tester, we can verify it’s working as expected. As you can see, our regex is capturing our test Visa CCN’s and missing a lot of false positives:

Visa Regex Test

Real World Ex: Blocking Emails with Credit Card Numbers in Office 365

Now that we’ve established our regex’s, let’s apply it to a real world example. For our example, we’ll block inbound/outbound email in Office 365. Please note, Office 365 transport rules only allow 128 characters in a regex, and due to this we’ll need to use multiple regex’s for matching Discover.  Also note, two of the Discover Regex’s I used above have a modified wrapper to keep them at the 128 character limit.

  1. Login to the Office 365 Admin Portal (https://portal.microsoftonline.com)
  2. Click Admin then click Exchange
  3. Under the Exchange Admin Center, click Mail Flow
  4. Click the Rules tab
  5. Click the + to create a new rule
    1. Name: Block Emails with Credit Card Numbers
    2. Apply this rule if: The subject or body matches:
      1. Paste each CCN Regex
    3. Do the following:  Reject the message with the explanation
      1. Rejection Reason: Your message was blocked due to the detection of a Credit Card Number
  6. Click Save
    1. Note: Mail flow rules normally take 30-35 minutes to replicate in Office 365
Apr 04

Block Outbound Email for Specific Users

Overview

There are a few situations where you may need to restrict certain users from sending email to external users.  For example, you may have part time employees that only need to send email to internal users OR you might have an employee who’s about to get terminated and don’t want them emailing clients.  Fortunately, in Office 365 Exchange you can create a Mail Flow Rule to accomplish this.

Create Distribution Group to Define Users to Block Outbound Email

In order for the mail flow rule to see the group, it must be a distribution group.  However, you can easily hide it from the GAL so your users don’t see it.  Many organizations use CustomAttribute15 to define what displays in there GAL.  If that’s your case, simply do not define CustomAttribute15 or define it to a value so it does not show in your GAL; otherwise, set the attribute to Hide group from Exchange Address Lists.

  1. Create a new distribution group
    1. Name: Block Outbound Email
    2. Email: blockoutboundemail@<company>.onmicrosoft.com
    3. Members: Add any user you want to block from sending outbound emails to external recipients (They will only be able to send to internal recipients)
  2. If you are using Office 365 in a Hybrid Deployment, make sure you use dirsync to synchronizes your new group

Create Mail Flow Rule

In this example, we will prevent a user from sending emails to any external recipients, but they will still be able to send to internal recipients.

  1. Login to the Office 365 Admin Portal https://portal.microsoftonline.com
  2. Click Admin then click Exchange to open the Exchange Admin CenterOpen Exchange Admin Center
  3. Click mail flow then click on the Rules tab
  4. Click the + symbol and click Create a new rule       Create New Rule
  5. Name the rule Block Outbound Emails to External Recipients
  6. Under Apply this rule if, click the recipient is located
    1. Select Outside the organization and click OK
  7. Click More Options to add another condition
  8. Click Add Condition
  9. On the new condition, select the sender is a member of this group
    1. Search and select the group Block Outbound Emails and click OK
    2. Note: Despite the wording stating “member of this group”, you can select a user instead of a group.  However, it’s easier to manage and you do not need to wait for the mail flow rule to propagate on 365, which can take up to an hour in my testing.
  10. Under Do the following, select Block the message then click delete the message without notifying anyone, and click OK
  11. Click Save

IMPORTANT NOTE:  It can take up to 45 minutes for Microsoft’s back end to fully synchronize rules!  This means any new or modified rules can take up to 45 minutes to take effect!

Block Outbound Email

 

 

 

Apr 04

Delivery Report in Outlook or Outlook Web App

Overview

When using Outlook or Outlook Web App (OWA) in an Office 365 or Exchange environment, you can track the message from the client side.  Both Outlook and OWA allow you to view a delivery report in order to confirm a message was delivered when the recipient claims they have not received it or if it’s taking a long time to deliver.  Delivery reports work for both internal and external recipients.

View a Delivery Report in Outlook

  1. In Outlook, go to your Sent Items folder
  2. Locate the message you want to track and open it
  3. Click File, click Info, and click Open Delivery Report

Outlook Message Delivery Report

View a Delivery Report in Outlook Web App (OWA)

If you are using any other email client than Outlook (mobile device, OWA, etc), you can use OWA to view a delivery report.

  1. Login to OWA at https://portal.microsoftonline.com
  2. Click the Gear Icon, then click Options
  3. Click Organize Email then click Delivery Reports
  4. Enter your search criteria, click Search
  5. Select the email you want to track and click the Pencil Icon to view the delivery report

OWA Delivery Report

Review Delivery Report

Internal delivery reports will show Delivered upon success delivering.  Also note, Office 365 Exchange only keeps message tracking data for 14 days.

Delivery Report Internal

External delivery reports will only show Transferred which means it successfully sent out from your mail server.  However, this does not guarantee the recipient received the email because there can be issues on the recipients email server.

Delivery Report External

Apr 04

365Licenses.ps1

#=========================================================================
# 365Licenses.ps1
# VERSION: 1.0
# AUTHOR: Brian Steinmeyer
# EMAIL: sigkill@sigkillit.com
# WEB: http://sigkillit.com
# DATE: 4/4/20114
# REQUIREMENTS:
# 1) Microsoft Online Services Sign-In Assistant for IT Professionals
# -(http://www.microsoft.com/en-gb/download/details.aspx?id=28177)
# 2) Windows Azure Active Directory Module for Windows PowerShell
# -(http://technet.microsoft.com/en-us/library/jj151815.aspx#bkmk_installmodule)
# COMMENTS: This script is intended to retrieve Office 365 licensing information
# by accepted domains. It will provide the active license count by domain,
# estimate the cost per domain, and provide the number of unused licenses.
#=========================================================================

# ------ SCRIPT CONFIGURATION ------
# Define License Unit Cost
$intCost = 3.88
# ------ END CONFIGURATION ------

# Connect to Microsoft Online
write-host "Connecting to Office 365..."
Import-Module MSOnline
Try {
Connect-MsolService -ErrorAction Stop
} Catch {
Write-Host $error[0].Exception -ForegroundColor Red -BackgroundColor Black
Write-Host "Error Connecting to Office 365... Quitting Script!" -ForegroundColor Red -BackgroundColor Black
Break
}

# Get Office 365 Accepted Domains and Active User Licenses
Try {
$arrDomains = @(Get-MsolDomain)
} Catch {
Write-Host "Error Retrieving Office 365 Accepted Domains... Quitting Script!" -ForegroundColor Red -BackgroundColor Black
Break
}
$arrCompany = @()
$TotalLicenses = 0
$TotalCost = 0
foreach ($d in $arrDomains){
$domain = $d.Name
write-host ("PROCESSING: " + $domain.ToUpper())
$users = Get-MsolUser -All | where {$_.isLicensed -eq "True" -and $_.UserPrincipalName.Contains($domain)} | Select DisplayName, UserPrincipalName -ExpandProperty Licenses | Select DisplayName, UserPrincipalName, AccountSkuID
If ($users.count -ne $null){
$i = $users.count
$users | format-table
$users | Export-Csv ("365_Licenses_" + $domain.replace(".","_") + ".csv")
}
Else{
$i = 0
Write-Host "0 Licenses<code>n</code>n"
}
$objCompany = New-Object -TypeName PSObject
$objCompany | Add-Member -Name 'Domain' -MemberType Noteproperty -Value $domain
$objCompany | Add-Member -Name 'Licenses' -MemberType Noteproperty -Value $i
$objCompany | Add-Member -Name 'Cost' -MemberType Noteproperty -Value ("{0:C2}" -f ($i * $intCost))
$arrCompany += $objCompany
$TotalLicenses += $i
$TotalCost += ($i * $intCost)

}

# Get Company Licensing Info
Try {
$companyLicenses = Get-MsolAccountSku | Select AccountSkuId, ActiveUnits, WarningUnits, ConsumedUnits
} Catch {
Write-Host $error[0].Exception -ForegroundColor Red -BackgroundColor Black
Write-Host "Error Retrieving Office 365 Account Info... Quitting Script!" -ForegroundColor Red -BackgroundColor Black
Break
}
$objCompany = New-Object -TypeName PSObject
$objCompany | Add-Member -Name 'Domain' -MemberType Noteproperty -Value "TOTAL ACTIVE LICENSES"
$objCompany | Add-Member -Name 'Licenses' -MemberType Noteproperty -Value $TotalLicenses
$objCompany | Add-Member -Name 'Cost' -MemberType Noteproperty -Value ("{0:C2}" -f $TotalCost)
$arrCompany += $objCompany

$unusedLicenses = ($companyLicenses.ActiveUnits - $companyLicenses.ConsumedUnits)
$unusedCost = ($unusedLicenses * $intCost)
$objCompany = New-Object -TypeName PSObject
$objCompany | Add-Member -Name 'Domain' -MemberType Noteproperty -Value "TOTAL UNUSED LICENSES"
$objCompany | Add-Member -Name 'Licenses' -MemberType Noteproperty -Value $unusedLicenses
$objCompany | Add-Member -Name 'Cost' -MemberType Noteproperty -Value ("{0:C2}" -f $unusedCost)
$arrCompany += $objCompany

$objCompany = New-Object -TypeName PSObject
$objCompany | Add-Member -Name 'Domain' -MemberType Noteproperty -Value "GRAND TOTAL LICENSES"
$objCompany | Add-Member -Name 'Licenses' -MemberType Noteproperty -Value $companyLicenses.ActiveUnits
$objCompany | Add-Member -Name 'Cost' -MemberType Noteproperty -Value ("{0:C2}" -f ($companyLicenses.ActiveUnits * $intCost))
$arrCompany += $objCompany

# Display Statistics
$companyLicenses | Format-Table -Auto
$arrCompany | Format-Table -Auto