Powershell – Blank AD Profile PATH

<#
.SYNOPSIS
  Clear Home Folder (local path) for each AD user in an OU and export results to CSV.

.PARAMETER SearchBase
  Distinguished Name (DN) of the OU to search.

.PARAMETER WhatIfMode
  If supplied, no changes are actually made.

.PARAMETER CsvPath
  Output path for the CSV file.

#>

param(
    [string]$SearchBase = 'OU=.Users (Role Based),OU=Steven Whiting,DC=whiting-steven,DC=co,DC=uk',
    [switch]$WhatIfMode,
    [string]$CsvPath = ".\HomeFolder_Clear_Report_$(Get-Date -Format yyyyMMdd_HHmmss).csv"
)

# Import AD module
Import-Module ActiveDirectory -ErrorAction Stop

Write-Output "SearchBase: $SearchBase"
if ($WhatIfMode) { Write-Output "WHATIF MODE ENABLED — no changes will be made." }

# Retrieve users
$users = Get-ADUser -Filter * -SearchBase $SearchBase -SearchScope Subtree -Properties homeDirectory,homeDrive,DistinguishedName,SamAccountName

if (-not $users) {
    Write-Output "No users found under the specified OU."
    exit
}

# Results array
$results = @()

foreach ($u in $users) {
    $result = [PSCustomObject]@{
        SamAccountName = $u.SamAccountName
        DistinguishedName = $u.DistinguishedName
        PreviousHomeDirectory = $u.homeDirectory
        PreviousHomeDrive = $u.homeDrive
        Action = ""
    }

    if ($WhatIfMode) {
        $result.Action = "Would Clear"
        $results += $result
        continue
    }

    try {
        Set-ADUser -Identity $u.DistinguishedName -Clear homeDirectory,homeDrive -Confirm:$false -ErrorAction Stop
        $result.Action = "Cleared"
    } catch {
        $result.Action = "Failed: $($_.Exception.Message)"
    }

    $results += $result
}

# Export CSV
$results | Export-Csv -NoTypeInformation -Path $CsvPath

Write-Output "CSV report created: $CsvPath"
Write-Output "Completed."

SMTP/Alias

For AD SMTP and Alias’

<#
.SYNOPSIS
    Export SMTP addresses (primary + secondary) from AD users.

.DESCRIPTION
    Uses Get-ADUser to collect the 'proxyAddresses' and 'mail' attributes.
    - Primary SMTP is the address with the "SMTP:" prefix (uppercase).
    - Other SMTP addresses are those with the "smtp:" prefix (lowercase).
    - If proxyAddresses is empty, the script will attempt to use the 'mail' attribute as primary.

.PARAMETER OutputCsv
    File path for exported CSV. Default: .\AD-SMTP-Addresses.csv

.PARAMETER SearchBase
    Optional AD container (distinguishedName) to restrict the search.

.PARAMETER IncludeDisabled
    If specified, include disabled accounts as well. By default disabled accounts are excluded.

.EXAMPLE
    .\Export-AD-SMTP.ps1 -OutputCsv C:\temp\smtp-addresses.csv

#>

param(
    [string]$OutputCsv = ".\AD-SMTP-Addresses.csv",
    [string]$SearchBase,
    [switch]$IncludeDisabled
)

# Ensure ActiveDirectory module is available
if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
    Write-Error "The ActiveDirectory module is not installed or available. Install RSAT/Active Directory module and run again."
    exit 1
}

Import-Module ActiveDirectory -ErrorAction Stop

# Build filter
$filter = if ($IncludeDisabled) { { } } else { { Enabled -eq $true } }

# Properties we need
$properties = @("proxyAddresses","mail","distinguishedName","samAccountName","displayName","objectClass")

try {
    if ($SearchBase) {
        $users = Get-ADUser -Filter * -SearchBase $SearchBase -Properties $properties
    } else {
        $users = Get-ADUser -Filter * -Properties $properties
    }
}
catch {
    Write-Error "Failed to query Active Directory: $_"
    exit 1
}

$result = foreach ($u in $users) {
    # Some objects may not have proxyAddresses (or not be users) - handle safely
    $proxy = @()
    if ($u.proxyAddresses) {
        $proxy = $u.proxyAddresses
    }

    # Normalize and split SMTP addresses
    $primary = $null
    $others  = @()

    if ($proxy.Count -gt 0) {
        # find exact uppercase SMTP: for primary
        $primaryEntry = $proxy | Where-Object { $_ -like "SMTP:*" } | Select-Object -First 1
        if ($primaryEntry) {
            $primary = $primaryEntry -replace '^[sS][mM][tT][pP]:',''  # remove prefix (case-insensitive)
        } else {
            # no uppercase SMTP found -> try any smtp: entry as fallback
            $fallback = $proxy | Where-Object { $_ -match '^(smtp|SMTP):' } | Select-Object -First 1
            if ($fallback) { $primary = $fallback -replace '^[sS][mM][tT][pP]:','' }
        }

        $others = $proxy |
            Where-Object { $_ -match '^(smtp|SMTP):' } |
            Where-Object { ($_ -replace '^[sS][mM][tT][pP]:','') -ne $primary } |
            ForEach-Object { $_ -replace '^[sS][mM][tT][pP]:','' }
    }

    # If no proxyAddresses, fall back to mail attribute as primary (if present)
    if (-not $primary -and $u.mail) {
        $primary = $u.mail
    }

    # Build object for export
    [PSCustomObject]@{
        DistinguishedName = $u.DistinguishedName
        SamAccountName    = $u.SamAccountName
        DisplayName       = $u.DisplayName
        ObjectClass       = $u.ObjectClass
        PrimarySMTP       = $primary
        OtherSMTPs        = if ($others.Count -gt 0) { $others -join ";" } else { $null }
    }
}

# Export to CSV
try {
    $result | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding UTF8
    Write-Output "Export completed: $OutputCsv ('$($result.Count) records)'"
}
catch {
    Write-Error "Failed to export CSV: $($_)"
    exit 1
}

Powershell – to change all users UNP from swhiting to steven.whiting

Wasn’t perfect but did what needed. Changed all user names from swhiting to steven.whiting. Doesn’t change the login so can still use the old swhiting for logging in, was just easier. Despite the UPN changing, it oddly doesn’t update the Entra ID when it syncs to Azure. Still looking into that, but this ended up being for the best. Cause of the way our domain was originally setup you have to sign in as swhiting@stevenwhiting.co.uk so having to do the full steven.whiting@ would be annoying.

<#!
.SYNOPSIS
    Update UPN and Primary SMTP Address for specific users identified by sAMAccountName (hybrid AD + Entra ID).

.DESCRIPTION
    - Targets ONLY the sAMAccountNames you specify (hard list below).
    - For each user, builds the new identity as firstname.surname@stevenwhiting.co.uk
      based on their AD GivenName + Surname (lowercased; non-alphanumerics removed).
    - Updates on-prem AD so hybrid sync flows changes to Entra ID/Exchange Online:
        * userPrincipalName (UPN)
        * mail (primary SMTP)
        * proxyAddresses (sets new Primary as 'SMTP:' and preserves old sAMAccountName as alias 'smtp:')
        * mailNickname (left part)
    - Writes a dated CSV log for auditing and rollback.
    - Includes rollback mode.

.PARAMETER AlsoSetExchangeOnline
    Attempt to set primary SMTP in Exchange Online as well (OFF by default; hybrid sync usually overwrites EXO-only changes).

.PARAMETER RollbackPath
    Path to a previous CSV log from this script to revert changes.

.NOTES
    Author: Steven Whiting – Using Chat GPT so may not be perfect but it worked and did what I needed it to do. Probably better ways to do it. With this script you also have to manually edit the users you want changed. I quite liked that for safety.
    Date:   2025-09-23

    This script uses CmdletBinding(SupportsShouldProcess=$true).
    Use -WhatIf on the script to preview, and -Confirm to be prompted.
	
	
#>

[CmdletBinding(SupportsShouldProcess=$true)]
param(
    [switch]$AlsoSetExchangeOnline,
    [string]$RollbackPath
)

Import-Module ActiveDirectory -ErrorAction Stop

# ---------- Helpers ----------
function Write-Info($msg)  { Write-Host "[INFO ] $msg" -ForegroundColor Cyan }
function Write-Warn($msg)  { Write-Warning $msg }
function Write-Err ($msg)  { Write-Host "[ERROR] $msg" -ForegroundColor Red }

function Get-PrimarySmtpFromProxies([string[]]$Proxies) {
    if (-not $Proxies) { return $null }
    $p = $Proxies | Where-Object { $_ -cmatch '^SMTP:' } | Select-Object -First 1
    if ($p) { return ($p -replace '^SMTP:','') }
    return $null
}

function Build-NewAddressParts([string]$GivenName,[string]$Surname,[string]$Domain) {
    if ([string]::IsNullOrWhiteSpace($GivenName) -or [string]::IsNullOrWhiteSpace($Surname)) {
        throw "GivenName/Surname missing; cannot build firstname.surname."
    }
    $fn = ($GivenName  -replace "[^A-Za-z0-9]", "").ToLower()
    $sn = ($Surname    -replace "[^A-Za-z0-9]", "").ToLower()
    $local = ("{0}.{1}" -f $fn, $sn)
    return @{ Local=$local; Upn=("{0}@{1}" -f $local,$Domain); Primary=("{0}@{1}" -f $local,$Domain); Nick=$local }
}

function Ensure-UniqueUpn([string]$CandidateUpn,[string]$DomainNC,[string]$UserDN) {
    $exists = Get-ADUser -Filter ("userPrincipalName -eq '{0}'" -f $CandidateUpn) -SearchBase $DomainNC -ErrorAction SilentlyContinue
    if (-not $exists -or ($exists.DistinguishedName -eq $UserDN)) { return $CandidateUpn }
    $prefix,$suffix = $CandidateUpn.Split('@')
    for ($i=1; $i -lt 1000; $i++) {
        $try = "{0}{1}@{2}" -f $prefix,$i,$suffix
        $exists = Get-ADUser -Filter ("userPrincipalName -eq '{0}'" -f $try) -SearchBase $DomainNC -ErrorAction SilentlyContinue
        if (-not $exists -or ($exists.DistinguishedName -eq $UserDN)) { return $try }
    }
    throw "Unable to find a unique UPN after 999 attempts for $CandidateUpn"
}

function Set-UserMailAttributes {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Microsoft.ActiveDirectory.Management.ADUser]$User,
        [string]$NewUpn,
        [string]$NewPrimarySmtp,
        [string]$NewMailNick,
        [string]$DomainSuffix
    )

    $dn = $User.DistinguishedName
    $currentProxies = @($User.proxyAddresses)

    # Demote any existing primary entries to alias form (lowercase)
    $proxiesNoPrimary = @()
    foreach ($p in $currentProxies) { $proxiesNoPrimary += ($p -replace '^SMTP:','smtp:') }

    # Add old sAMAccountName as alias
    $newAliasAdded = "$($User.SamAccountName)@$DomainSuffix"
    if ($proxiesNoPrimary -notcontains ("smtp:$newAliasAdded")) {
        Write-Info "Adding old sAMAccountName as alias: smtp:$newAliasAdded"
        $proxiesNoPrimary += "smtp:$newAliasAdded"
    }

    # Remove duplicates and any lingering entry that collides with new primary
    $proxiesNoPrimary = $proxiesNoPrimary | Sort-Object -Unique | Where-Object { $_ -ne ("smtp:$NewPrimarySmtp") -and $_ -ne ("SMTP:$NewPrimarySmtp") }

    # Final list: new primary first, then aliases
    $finalProxies = @("SMTP:$NewPrimarySmtp") + $proxiesNoPrimary

    # Ensure array of strings
    if ($finalProxies -isnot [array]) { $finalProxies = @($finalProxies) }
    $finalProxies = $finalProxies | ForEach-Object { [string]$_ }

    $replace = @{
        proxyAddresses = $finalProxies
        mail           = $NewPrimarySmtp
        mailNickname   = $NewMailNick
    }

    if ($PSCmdlet.ShouldProcess($User.SamAccountName, "Set mail attributes and UPN")) {
        Set-ADUser -Identity $dn -UserPrincipalName $NewUpn -Replace $replace -ErrorAction Stop
    }

    return @{
        NewProxies     = $finalProxies
        NewAliasAdded  = "smtp:$newAliasAdded"
    }
}

# ---------- Config ----------
$DomainSuffix = 'stevenwhiting.co.uk'
$DomainNC     = 'DC=stevenwhiting,DC=co,DC=uk'
$SearchBaseDN = 'OU=IT,OU=formation,OU=.Users,OU=Steven Whiting,DC=stevenwhiting,DC=co,DC=uk'
$SamAccountNames = @('jwhiting','ldave','swhiting')

# ---------- Resolve targets ----------
$ResolvedUsers = @()
foreach ($sam in $SamAccountNames) {
    $found = Get-ADUser -Filter "SamAccountName -eq '$sam'" -SearchBase $SearchBaseDN -Properties GivenName,Surname,mail,mailNickname,proxyAddresses,DistinguishedName,SamAccountName,userPrincipalName
    if (-not $found) { Write-Err "User not found in OU scope: $sam"; continue }
    if ($found.Count -gt 1) { Write-Err "Multiple matches for sAMAccountName $sam in the scope. Aborting for safety."; exit 1 }
    $ResolvedUsers += $found
}

if ($ResolvedUsers.Count -ne $SamAccountNames.Count) {
    Write-Err "Expected to resolve $($SamAccountNames.Count) users, but resolved $($ResolvedUsers.Count). Aborting."
    return
}

# Confirmation
Write-Host "The following users will be updated:" -ForegroundColor Yellow
$ResolvedUsers | Select-Object SamAccountName, UserPrincipalName, GivenName, Surname, DistinguishedName | Format-Table -AutoSize
$ans = Read-Host "Type YES to proceed (anything else aborts)"
if ($ans -ne 'YES') { Write-Host 'Aborted by operator.'; return }

# ---------- Processing ----------
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$LogPath   = Join-Path -Path (Get-Location) -ChildPath ("UPN_SMTP_Change_Log_{0}.csv" -f $timestamp)
$logRows   = @()

foreach ($user in $ResolvedUsers) {
    try {
        $parts = Build-NewAddressParts -GivenName $user.GivenName -Surname $user.Surname -Domain $DomainSuffix
        $targetUpn = Ensure-UniqueUpn -CandidateUpn $parts.Upn -DomainNC $DomainNC -UserDN $user.DistinguishedName
        $oldPrimarySmtp = Get-PrimarySmtpFromProxies $user.proxyAddresses
        if (-not $oldPrimarySmtp -and $user.mail) { $oldPrimarySmtp = $user.mail }

        # Skip if already changed
        if ($user.UserPrincipalName -eq $targetUpn -and $oldPrimarySmtp -eq $parts.Primary) {
            Write-Host "$($user.SamAccountName) is already updated. Skipping..." -ForegroundColor Yellow
            $logRows += [pscustomobject]@{
                When                = (Get-Date)
                DN                  = $user.DistinguishedName
                SamAccountName      = $user.SamAccountName
                DisplayName         = $user.Name
                OldUPN              = $user.UserPrincipalName
                NewUPN              = $targetUpn
                OldPrimarySMTP      = $oldPrimarySmtp
                NewPrimarySMTP      = $parts.Primary
                FinalProxyAddresses = ($user.proxyAddresses -join ';')
                NewAliasAdded       = "$($user.SamAccountName)@$DomainSuffix"
                Result              = "Already changed"
            }
            continue
        }

        Write-Info ("Processing {0}:" -f $user.SamAccountName)
        $result = Set-UserMailAttributes -User $user -NewUpn $targetUpn -NewPrimarySmtp $parts.Primary -NewMailNick $parts.Nick -DomainSuffix $DomainSuffix

        Write-Host "  Final ProxyAddresses:"
        foreach ($proxy in $result.NewProxies) {
            if ($proxy -cmatch '^SMTP:') { Write-Host "    $proxy (Primary)" -ForegroundColor Green }
            else { Write-Host "    $proxy (Alias)" -ForegroundColor DarkGray }
        }
        Write-Host "  New alias added: $($result.NewAliasAdded)" -ForegroundColor Magenta

        $logRows += [pscustomobject]@{
            When                = (Get-Date)
            DN                  = $user.DistinguishedName
            SamAccountName      = $user.SamAccountName
            DisplayName         = $user.Name
            OldUPN              = $user.UserPrincipalName
            NewUPN              = $targetUpn
            OldPrimarySMTP      = $oldPrimarySmtp
            NewPrimarySMTP      = $parts.Primary
            FinalProxyAddresses = ($result.NewProxies -join ';')
            NewAliasAdded       = $result.NewAliasAdded
            Result              = "Success"
        }
    }
    catch {
        Write-Err $_.Exception.Message
    }
}

$logRows | Export-Csv -NoTypeInformation -Path $LogPath
Write-Info "Log written to: $LogPath"

Powershell – Move files based on date to specific folders

My Samsung S8 always saves photos in the format: 20180101_001741 so the year, month and day is at the start. I was grabbing all files off the phone and sticking them in one folder on another drive as back up. Been doing this for several years so now its a mass of over 30k files from several years. Was causing Explorer to take ages to sort. So wanted to move the files to specific folders based on the year. Was taking ages doing manually so, with the help of ChatGPT and Reddit (mainly Reddit & surfingoldelephant) put this together:

<# Get files in the specified folder, ONLY files. #>

$sourceFiles = Get-ChildItem -Path 'F:\PhoneBackup\19 08 2023\SD card\DCIM\Camera' -File

<# Set the backup path. Same as sourcefiles because we get the first 4 characters of the file name and move to that folder. #>

$backupPath  = 'F:\PhoneBackup\19 08 2023\SD card\DCIM\Camera'

<# For each $file (variable declared here as its thrown away after) in sourceFiles, set $year to be the first 4 characters of the file name. #>

foreach ($file in $sourceFiles) {
    $year = $file.Name.Substring(0, 4)

    <# From reading the IO.PATH is a class (so you don't have to write all the code out. This handles folder paths)
    The combine takes the two arguements $backupPath and $year and using the IO.Path class, combines them to give a valid
    path (IO.Path sorts that) and stores that in $destDirPath #>

    $destDirPath = [IO.Path]::Combine($backupPath, $year)

    <# Void supress' the output for the following command so you don't see anything
    The command creates a new item in the $destDirPath, itemtype is a directory. The -Force just does it without confirmation. #>

    [void] (New-Item -Path $destDirPath -ItemType 'Directory' -Force)

    <# Move the item from its current path to the destination path.#>

    Move-Item -Path $file.FullName -Destination $destDirPath
}

<# Output that script has finished. #>

Write-Host "Finished moving."

Powershell – replace characters

Another one and I ended up sticking a load of notes on this for myself

Get-Item “\\SERVERNAME\files\All – all general shared files\April 2020 – Rents and service charges\Archive*.pdf” | ForEach-Object {
Rename-Item $_ ($_.Name -replace “-“, ” “) -WhatIf }

So here are my notes on the command.

Get-Item : We’re getting the contents of the folder on SERVERNAME and looking at all files ending in .pdf

We have to put the path in “” because the folder name has spaces.

We then pipe this with the pipe command (cause it looks like a pipe) | to the ForEach-Object loop. The pipe command means you pass the results of what was just to the left of it, to the next command on its right.

ForEach-Object takes each file, from the Get-Item command and with Rename-Item stores each file’s name in the Powershell global variable $_. You use that global variable because its easy, its short (typing wise) and you don’t have to declare it at the top of your script like other variables. So less code. The $_. global variable is built into Powershell.

The $_.Name takes the contents stored in Global Variable $_ and adds it to name with the (in this case) – removed from all the files that had it and replaced with a space. That is what “-” ” ” are. You’re looking for “-” in the file name and replacing with ” ” space.

The whatif is only there so the command doesn’t actually change anything, it just shows what would happen if the command ran, it can be removed once you know the script works.

Powershell for deleting files with specific characters in the name.

Been attempting to learn Powershell, slowly. Needed help with this. Got part of it working but not fully until someone fixed it.

get-childitem -recurse | Where-Object name -Like ‘(2)‘ | ForEach-Object { remove-item -LiteralPath $_.fullname -whatif }

A shorter version

get-childitem -recurse -filter ‘*(2)*’ | remove-item -whatif

The -whatif will run the command but not execute it so NOTHING will be delete. It will just show you what it would do IF you executed the command.

I used this as my sister had loads of photos that I had to back up with

0dd123.jpg, Odd123 (2).jpg

Which was going to be an arse to go through and delete all the (2) copies.

Be aware if you are copying this script from my site because the ‘ are formatted different on the site and I believe if you paste them into Powershell it may not work. Just go and manually replace the ‘. They look exactly the same but the code behind them is somewhat different. I’ve had this happen a few times with code I’ve copied from somewhere that had ‘ in it.