PSGSuite.psm1

Param
(
    [parameter(Position = 0,ValueFromRemainingArguments = $true)]
    [AllowNull()]
    [Byte[]]
    $EncryptionKey = $null,
    [parameter(Position = 1)]
    [AllowNull()]
    [String]
    $ConfigName
)
$ModuleRoot = $PSScriptRoot
New-Variable -Name PSGSuiteKey -Value $EncryptionKey -Scope Global -Force
function Convert-Base64 {
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [ValidateSet("NormalString","Base64String","WebSafeBase64String")]
        [ValidateScript( {if ($_ -eq $To) {
                    throw "The 'From' parameter must not be the same as the 'To' parameter"
                }
                else {
                    $true
                }})]
        [String]
        $From,
        [parameter(Mandatory = $true,Position = 1)]
        [ValidateSet("NormalString","Base64String","WebSafeBase64String")]
        [ValidateScript( {if ($_ -eq $From) {
                    throw "The 'To' parameter must not be the same as the 'From' parameter"
                }
                else {
                    $true
                }})]
        [String]
        $To,
        [parameter(Mandatory = $true,Position = 2,ValueFromPipeline = $true)]
        [String]
        $String,
        [parameter(Mandatory = $false)]
        [String]
        $OutFile
    )
    if ($From -eq "NormalString") {
        $String = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($String))
    }
    elseif ($From -eq "WebSafeBase64String") {
        $String = $String.Replace('_', '/').Replace('-', '+').Replace('|','=')
        switch ($String.Length % 4) {
            2 {
                $String += "=="
            }
            3 {
                $String += "="
            }
        }
    }
    if ($To -eq "NormalString") {
        $String = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($String))
    }
    elseif ($To -eq "WebSafeBase64String") {
        $String = $String.TrimEnd("=").Replace('+', '-').Replace('/', '_');
    }
    if ($OutFile) {
        $String | Set-Content $OutFile -Force
    }
    else {
        return $String
    }
}
function Convert-DateToEpoch {
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true)]
        [datetime]
        $Date
    )
    Begin {
        $UnixEpoch = [timezone]::CurrentTimeZone.ToLocalTime([datetime]'1/1/1970')
    }
    Process {
        $result = (("$(($Date - $UnixEpoch).TotalMilliseconds)" -split "\.") -split "\,")[0]
    }
    End {
        return $result
    }
}
function Convert-EpochToDate {
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true)]
        [string]
        $EpochString
    )
    Begin {
        $UnixEpoch = [timezone]::CurrentTimeZone.ToLocalTime([datetime]'1/1/1970')
    }
    Process {
        try {
            $result = $UnixEpoch.AddSeconds($EpochString)
        }
        catch {
            try {
                $result = $UnixEpoch.AddMilliseconds($EpochString)
            }
            catch {
                $result = $UnixEpoch.AddTicks($EpochString)
            }
        }
    }
    End {
        return $result
    }
}
function Get-GSDecryptedConfig {
    [CmdletBinding()]
    Param(
        [parameter(Position = 0,ValueFromPipeline,Mandatory)]
        [object]
        $Config,
        [parameter(Position = 1,Mandatory)]
        [string]
        $ConfigName,
        [parameter(Position = 2)]
        [string]
        $ConfigPath
    )
    Process {
        $Config | Select-Object -Property `
            @{l = 'ConfigName';e = { $ConfigName }},
            @{l = 'P12KeyPath'; e = { Invoke-GSDecrypt $_.P12KeyPath } },
            'P12Key',
            @{l = 'P12KeyPassword'; e = { Invoke-GSDecrypt $_.P12KeyPassword } },
            @{l = 'P12KeyObject'; e = { Invoke-GSDecrypt $_.P12KeyObject } },
            @{l = 'ClientSecretsPath'; e = { Invoke-GSDecrypt $_.ClientSecretsPath } },
            @{l = 'ClientSecrets'; e = { Invoke-GSDecrypt $_.ClientSecrets } },
            @{l = 'AppEmail'; e = {
                    if ($_.AppEmail) {
                        Invoke-GSDecrypt $_.AppEmail
                    }
                    elseif ($_.ClientSecrets) {
                        (Invoke-GSDecrypt $_.ClientSecrets | ConvertFrom-Json).client_email
                    }
                }
            },
            @{l = 'AdminEmail'; e = { Invoke-GSDecrypt $_.AdminEmail } },
            @{l = 'CustomerID'; e = { Invoke-GSDecrypt $_.CustomerID } },
            @{l = 'Domain'; e = { Invoke-GSDecrypt $_.Domain } },
            @{l = 'Preference'; e = { Invoke-GSDecrypt $_.Preference } },
            @{l = 'ServiceAccountClientID'; e = {
                    if ($_.ServiceAccountClientID) {
                        Invoke-GSDecrypt $_.ServiceAccountClientID
                    }
                    elseif ($_.ClientSecrets) {
                        (Invoke-GSDecrypt $_.ClientSecrets | ConvertFrom-Json).client_id
                    }
                }
            },
            @{l = 'Chat'; e = {
                    $dict = @{
                        Webhooks = @{ }
                        Spaces   = @{ }
                    }
                    foreach ($key in $_.Chat.Webhooks.Keys) {
                        $dict['Webhooks'][$key] = (Invoke-GSDecrypt $_.Chat.Webhooks[$key])
                    }
                    foreach ($key in $_.Chat.Spaces.Keys) {
                        $dict['Spaces'][$key] = (Invoke-GSDecrypt $_.Chat.Spaces[$key])
                    }
                    $dict
                }
            },
            @{l = 'ConfigPath'; e = {
                if ($ConfigPath) {(Resolve-Path $ConfigPath).Path} elseif ($_.ConfigPath) {$_.ConfigPath} else {$null}
            }}
    }
}
function Invoke-GSDecrypt {
    param($String)
    if ($String -is [System.Security.SecureString]) {
        [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR(
            [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR(
                $String
            )
        )
    }
    elseif ($String -is [ScriptBlock]) {
        $String.InvokeReturnAsIs()
    }
    else {
        $String
    }
}

Function Invoke-GSEncrypt {
    param($string)
    if ($string -is [System.String] -and -not [String]::IsNullOrEmpty($String)) {
        ConvertTo-SecureString -String $string -AsPlainText -Force
    }
    else {
        $string
    }
}

function Get-MimeType {
    Param
    (
        [parameter(Mandatory=$true, ValueFromPipeline=$true,Position = 0)]
        [System.IO.FileInfo]
        $File
    )
    $mimeHash = @{
        arj = 'application/arj'
        bmp = 'image/bmp'
        cab = 'application/cab'
        csv = 'text/plain'
        doc = 'application/msword'
        gif = 'image/gif'
        htm = 'text/html'
        html = 'text/html'
        jpg = 'image/jpeg'
        js = 'text/js'
        log = 'text/plain'
        md = 'text/plain'
        mp3 = 'audio/mpeg'
        ods = 'application/vnd.oasis.opendocument.spreadsheet'
        pdf  = 'application/pdf'
        php = 'application/x-httpd-php'
        png = 'image/png'
        rar = 'application/rar'
        swf = 'application/x-shockwave-flash'
        tar = 'application/tar'
        tmpl = 'text/plain'
        txt = 'text/plain'
        xls = 'application/vnd.ms-excel'
        xlsx = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        xml = 'text/xml'
        zip = 'application/zip'
    }
    if ($File.PSIsContainer) {
        'application/vnd.google-apps.folder'
    }
    elseif ($mime = $mimeHash["$($file.Extension.TrimStart('.'))"]) {
        $mime
    }
    else {
        'application/octet-stream'
    }
}
function Get-PSGSuiteConfigNames {
    <#
    .SYNOPSIS
    Gets the current config names for tab completion on Switch-PSGSuiteConfig.
 
    .DESCRIPTION
    Gets the current config names for tab completion on Switch-PSGSuiteConfig.
    #>

    [cmdletbinding()]
    Param ()
    $fullConf = Import-SpecificConfiguration -CompanyName 'SCRT HQ' -Name 'PSGSuite' -Scope $Script:ConfigScope
    $fullConf.Keys | Where-Object {$_ -ne 'DefaultConfig'}
}

function Get-SafeFileName {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory,ValueFromPipeline,Position = 0)]
        [String]
        $Name
    )
    Process {
        $Name -replace "[$([RegEx]::Escape("$(([System.IO.Path]::GetInvalidFileNameChars() + [System.IO.Path]::GetInvalidPathChars()) -join '')"))]","_"
    }
}

function Import-GoogleSDK {
    [CmdletBinding()]
    Param()
    Process {
        $lib = Resolve-Path "$($script:ModuleRoot)\lib"
        $refs = @()
        $sdkPath = if ($PSVersionTable.PSVersion.Major -lt 6) {
            Write-Verbose "Importing the SDK's for net45"
            "$lib\net45"
        }
        else {
            Write-Verbose "Importing the SDK's for netstandard1.3"
            "$lib\netstandard1.3"
        }
        $dlls = Get-ChildItem $sdkPath -Filter "*.dll"
        $googleCore = ($dlls | Where-Object {$_.Name -eq 'Google.Apis.dll'})
        $googleCoreVersion = [System.Version]((Get-Item $googleCore.FullName).VersionInfo.FileVersion)
        $batches = @(
            # Load the non-Google DLLs first...
            ($dlls | Where-Object {$_.Name -notin $refs -and $_.Name -notmatch '^Google'})
            # Then load Google.Apis.dll...
            $googleCore
            # Then load the rest of the Google DLLs
            ($dlls | Where-Object {$_.Name -notin $refs -and $_.Name -match '^Google' -and $_.Name -ne 'Google.Apis.dll'})
        )
        foreach ($batch in $batches) {
            $batch | ForEach-Object {
                $sdk = $_.Name
                try {
                    $params = @{}
                    if ($_.Name -match '^Google' -and $_.Name -ne 'Google.Apis.dll' -and ([System.Version]((Get-Item $_.FullName).VersionInfo.FileVersion)) -ge $googleCoreVersion) {
                        $params['ReferencedAssemblies'] = ($dlls | Where-Object {$_.Name -eq 'Google.Apis.dll'}).FullName
                        Add-Type -Path $_.FullName @params -ErrorAction Stop
                    }
                    elseif ($_.Name -notmatch '^Google' -or $_.Name -eq 'Google.Apis.dll' -or ($_.Name -ne 'Google.Apis.dll' -and ([System.Version]((Get-Item $_.FullName).VersionInfo.FileVersion)) -ge $googleCoreVersion)) {
                        Add-Type -Path $_.FullName @params -ErrorAction Stop
                    }
                }
                catch [System.Reflection.ReflectionTypeLoadException] {
                    Write-Error "$($sdk): Unable to load assembly!"
                    Write-Host "Message: $($_.Exception.Message)"
                    Write-Host "StackTrace: $($_.Exception.StackTrace)"
                    Write-Host "LoaderExceptions: $($_.Exception.LoaderExceptions)"
                }
                catch {
                    Write-Error "$($sdk): $($_.Exception.Message)"
                }
            }
        }
    }
}

function Import-SpecificConfiguration {
    <#
    .Synopsis
    Import the full, layered configuration for the module.
    Allows for specification of scoped module to load, in case different scopes have different encryption levels
    .Description
    Imports the DefaultPath Configuration file, and then imports the Machine, Roaming (enterprise), and local config files, if they exist.
    Each configuration file is layered on top of the one before (so only needs to set values which are different)
    .Example
    $Configuration = Import-Configuration
 
    This example shows how to use Import-Configuration in your module to load the cached data
 
    .Example
    $Configuration = Get-Module Configuration | Import-Configuration
 
    This example shows how to use Import-Configuration in your module to load data cached for another module
    #>

    [CmdletBinding(DefaultParameterSetName = '__CallStack')]
    param(
        # A callstack. You should not ever pass this.
        # It is used to calculate the defaults for all the other parameters.
        [Parameter(ParameterSetName = "__CallStack")]
        [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack),

        # The Module you're importing configuration for
        [Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true)]
        [System.Management.Automation.PSModuleInfo]$Module = $(
            $mi = ($CallStack)[0].InvocationInfo.MyCommand.Module
            if ($mi -and $mi.ExportedCommands.Count -eq 0) {
                if ($mi2 = Get-Module $mi.ModuleBase -ListAvailable | Where-Object Name -eq $mi.Name | Where-Object ExportedCommands | Select-Object -First 1) {
                    return $mi2
                }
            }
            return $mi
        ),

        # An optional module qualifier (by default, this is blank)
        [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("Author")]
        [String]$CompanyName = $(
            if ($Module) {
                $Name = $Module.CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]","_"
                if ($Name -eq "Unknown" -or -not $Name) {
                    $Name = $Module.Author
                    if ($Name -eq "Unknown" -or -not $Name) {
                        $Name = "AnonymousModules"
                    }
                }
                $Name
            }
            else {
                "AnonymousScripts"
            }
        ),

        # The name of the module or script
        # Will be used in the returned storage path
        [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]$Name = $(if ($Module) {
                $Module.Name
            }),

        # The full path (including file name) of a default Configuration.psd1 file
        # By default, this is expected to be in the same folder as your module manifest, or adjacent to your script file
        [Parameter(ParameterSetName = "ManualOverride", ValueFromPipelineByPropertyName = $true)]
        [Alias("ModuleBase")]
        [String]$DefaultPath = $(if ($Module) {
                Join-Path $Module.ModuleBase Configuration.psd1
            }),

        [Parameter(ParameterSetName = "Path",Mandatory = $true)]
        [ValidateScript( {Test-Path $_})]
        [string]$Path,

        [Parameter(Mandatory = $false)]
        [ValidateSet("User", "Machine", "Enterprise", $null)]
        [string]$Scope = $Script:ConfigScope,

        # The version for saved settings -- if set, will be used in the returned path
        # NOTE: this is *never* calculated, if you use version numbers, you must manage them on your own
        [Version]$Version,

        # If set (and PowerShell version 4 or later) preserve the file order of configuration
        # This results in the output being an OrderedDictionary instead of Hashtable
        [Switch]$Ordered
    )
    begin {
        Write-Verbose "Module Name $Name"
    }
    process {
        if (!$Name) {
            throw "Could not determine the configuration name. When you are not calling Import-Configuration from a module, you must specify the -Author and -Name parameter"
        }

        if (Test-Path $DefaultPath -Type Container) {
            $DefaultPath = Join-Path $DefaultPath Configuration.psd1
        }
        $Configuration = if (Test-Path $DefaultPath) {
            Import-Metadata $DefaultPath -ErrorAction Ignore -Ordered:$Ordered
            Write-Verbose "Default config found: $DefaultPath"
        }
        else {
            @{}
        }

        $Parameters = @{
            CompanyName = $CompanyName
            Name        = $Name
        }
        if ($Version) {
            $Parameters.Version = $Version
        }
        if ($Path) {
            Write-Verbose "Importing configuration from specific path: $Path"
            Import-Metadata "$(Resolve-Path $Path)" -ErrorAction Ignore -Ordered:$Ordered
        }
        else {
            if (!$Scope -or $Scope -eq "Machine") {
                $MachinePath = Get-ConfigurationPath @Parameters -Scope Machine
                $MachinePath = Join-Path $MachinePath Configuration.psd1
                $Machine = if (Test-Path $MachinePath) {
                    Import-Metadata $MachinePath -ErrorAction Ignore -Ordered:$Ordered
                    Write-Verbose "Machine config found: $MachinePath"
                }
                else {
                    @{}
                }
            }

            if (!$Scope -or $Scope -eq "Enterprise") {
                $EnterprisePath = Get-ConfigurationPath @Parameters -Scope Enterprise
                $EnterprisePath = Join-Path $EnterprisePath Configuration.psd1
                $Enterprise = if (Test-Path $EnterprisePath) {
                    Import-Metadata $EnterprisePath -ErrorAction Ignore -Ordered:$Ordered
                    Write-Verbose "Enterprise config found: $EnterprisePath"
                }
                else {
                    @{}
                }
            }

            if (!$Scope -or $Scope -eq "User") {
                $LocalUserPath = Get-ConfigurationPath @Parameters -Scope User
                $LocalUserPath = Join-Path $LocalUserPath Configuration.psd1
                $LocalUser = if (Test-Path $LocalUserPath) {
                    Import-Metadata $LocalUserPath -ErrorAction Ignore -Ordered:$Ordered
                    Write-Verbose "LocalUser config found: $LocalUserPath"
                }
                else {
                    @{}
                }
            }
            switch ($Scope) {
                Machine {
                    Write-Verbose "Importing configuration at scope: $Scope"
                    $Configuration | Update-Object $Machine
                }
                Enterprise {
                    Write-Verbose "Importing configuration at scope: $Scope"
                    $Configuration | Update-Object $Enterprise
                }
                User {
                    Write-Verbose "Importing configuration at scope: $Scope"
                    $Configuration | Update-Object $LocalUser
                }
                Default {
                    Write-Verbose "Importing layered configuration"
                    $Configuration |
                        Update-Object $Machine |
                        Update-Object $Enterprise |
                        Update-Object $LocalUser
                }
            }
        }
    }
}

function Get-LicenseSkus {
    ConvertFrom-Json '{"Google-Drive-storage-1TB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 1TB", "aliases": ["drive1tb", "1tb", "googledrivestorage1tb"]}, "Google-Apps-For-Government": {"product": "Google-Apps", "displayName": "G Suite Government", "aliases": ["gafg", "gsuitegovernment", "gsuitegov"]}, "Google-Vault": {"product": "Google-Vault", "displayName": "Google Vault", "aliases": ["vault", "googlevault"]}, "Google-Drive-storage-8TB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 8TB", "aliases": ["drive8tb", "8tb", "googledrivestorage8tb"]}, "Google-Drive-storage-400GB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 400GB", "aliases": ["drive400gb", "400gb", "googledrivestorage400gb"]}, "Google-Drive-storage-16TB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 16TB", "aliases": ["drive16tb", "16tb", "googledrivestorage16tb"]}, "1010340002": {"product": "101034", "displayName": "G Suite Business Archived", "aliases": ["gsbau", "businessarchived", "gsuitebusinessarchived"]}, "1010340001": {"product": "101034", "displayName": "G Suite Enterprise Archived", "aliases": ["gseau", "enterprisearchived", "gsuiteenterprisearchived"]}, "Google-Drive-storage-50GB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 50GB", "aliases": ["drive50gb","50gb", "googledrivestorage50gb"]}, "Google-Apps": {"product": "Google-Apps", "displayName": "G Suite Free/Standard", "aliases": ["standard", "free"]}, "Google-Drive-storage-4TB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 4TB", "aliases": ["drive4tb", "4tb", "googledrivestorage4tb"]}, "Google-Drive-storage-2TB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 2TB", "aliases": ["drive2tb", "2tb", "googledrivestorage2tb"]}, "Google-Apps-For-Postini": {"product": "Google-Apps", "displayName": "G Suite Message Security", "aliases": ["gams", "postini", "gsuitegams", "gsuitepostini", "gsuitemessagesecurity"]}, "Google-Apps-Unlimited": {"product": "Google-Apps", "displayName": "G Suite Business", "aliases": ["gau", "gsb", "unlimited", "gsuitebusiness"]}, "Google-Drive-storage-200GB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 200GB", "aliases": ["drive200gb", "200gb", "googledrivestorage200gb"]}, "1010010001": {"product": "101001", "displayName": "Cloud Identity", "aliases": ["identity", "cloudidentity"]}, "Google-Coordinate": {"product": "Google-Coordinate", "displayName": "Google Coordinate", "aliases": ["coordinate", "googlecoordinate"]}, "1010330002": {"product": "101033", "displayName": "Google Voice Premier", "aliases": ["gvpremier", "voicepremier", "googlevoicepremier"]}, "1010330003": {"product": "101033", "displayName": "Google Voice Starter", "aliases": ["gvstarter", "voicestarter", "googlevoicestarter"]}, "1010330004": {"product": "101033", "displayName": "Google Voice Standard", "aliases": ["gvstandard", "voicestandard", "googlevoicestandard"]}, "Google-Vault-Former-Employee": {"product": "Google-Vault", "displayName": "Google Vault Former Employee", "aliases": ["vfe", "googlevaultformeremployee"]}, "Google-Apps-For-Business": {"product": "Google-Apps", "displayName": "G Suite Basic", "aliases": ["gafb", "gafw", "basic", "gsuitebasic"]}, "1010060001": {"product": "Google-Apps", "displayName": "Drive Enterprise", "aliases": ["d4e", "driveenterprise", "drive4enterprise"]}, "1010020020": {"product": "Google-Apps", "displayName": "G Suite Enterprise", "aliases": ["gae", "gse", "enterprise", "gsuiteenterprise"]}, "Google-Apps-Lite": {"product": "Google-Apps", "displayName": "G Suite Lite", "aliases": ["gal", "gsl", "lite", "gsuitelite"]}, "1010050001": {"product": "101005", "displayName": "Cloud Identity Premium", "aliases": ["identitypremium", "cloudidentitypremium"]}, "1010310002": {"product": "101031", "displayName": "G Suite Enterprise for Education", "aliases": ["gsefe", "e4e", "gsuiteenterpriseeducation"]}, "1010310003": {"product": "101031", "displayName": "G Suite Enterprise for Education Student", "aliases": ["gsefes", "e4es", "gsuiteenterpriseeducationstudent"]}, "Google-Chrome-Device-Management": {"product": "Google-Chrome-Device-Management", "displayName": "Google Chrome Device Management", "aliases": ["chrome", "cdm", "googlechromedevicemanagement"]}, "Google-Drive-storage-20GB": {"product": "Google-Drive-storage", "displayName": "Google Drive Storage 20GB", "aliases": ["drive20gb", "20gb", "googledrivestorage20gb"]}}'
}

function Get-LicenseProducts {
    ConvertFrom-Json '{"Google-Vault": "Google Vault", "Google-Drive-storage": "Google Drive Storage", "Google-Coordinate": "Google Coordinate", "101034": "G Suite Archived", "101033": "Google Voice", "Google-Apps": "G Suite", "101031": "G Suite Enterprise for Education", "101006": "Drive Enterprise", "Google-Chrome-Device-Management": "Google Chrome Device Management", "101005": "Cloud Identity Premium", "101001": "Cloud Identity Free"}'
}

function Get-LicenseProductHash {
    Param (
        [Parameter(Position = 0)]
        [String]
        $Key
    )
    $_result = @{'Cloud-Identity' = '101001'}
    $_full = Get-LicenseProducts
    $_products = $_full.PSObject.Properties.Name | Sort-Object -Unique
    foreach ($_id in $_products) {
        $_friendlyname = $_full.$_id -replace '\W+','-'
        $_result[$_id] = $_id
        $_result[$_friendlyname] = $_id
    }
    if ($Key) {
        $_result[$Key]
    }
    else {
        $_result
    }
}

function Get-LicenseSkuHash {
    Param (
        [Parameter(Position = 0)]
        [String]
        $Key
    )
    $_result = @{}
    $_full = Get-LicenseSkus
    $_licenses = $_full.PSObject.Properties.Name | Sort-Object -Unique
    foreach ($_sku in $_licenses) {
        $_displayName = $_full.$_sku.displayName -replace '\W+','-'
        $_result[$_displayName] = $_sku
        foreach ($_alias in $_full.$_sku.aliases) {
            $_result[$_alias] = $_sku
        }
    }
    if ($Key) {
        $_result[$Key]
    }
    else {
        $_result
    }
}

function Get-LicenseSkuToProductHash {
    Param (
        [Parameter(Position = 0)]
        [String]
        $Key
    )
    $_result = @{}
    $_full = Get-LicenseSkus
    $_licenses = $_full.PSObject.Properties.Name | Sort-Object -Unique
    foreach ($_sku in $_licenses) {
        $_displayName = $_full.$_sku.displayName -replace '\W+','-'
        $_result[$_sku] = $_full.$_sku.product
        $_result[$_displayName] = $_full.$_sku.product
        foreach ($_alias in $_full.$_sku.aliases) {
            $_result[$_alias] = $_full.$_sku.product
        }
    }
    if ($Key) {
        $_result[$Key]
    }
    else {
        $_result
    }
}

function Get-LicenseSkuFromDisplayName  {
    Param (
        [Parameter(Position = 0)]
        [String]
        $Key
    )
    $_result = @{}
    $_full = Get-LicenseSkus
    $_licenses = $_full.PSObject.Properties.Name | Sort-Object -Unique
    foreach ($_sku in $_licenses) {
        $_displayName = $_full.$_sku.displayName -replace '\W+','-'
        $_result[$_displayName] = $_sku
    }
    if ($Key) {
        $_result[$Key]
    }
    else {
        $_result
    }
}

function Get-LicenseProductFromDisplayName {
    Param (
        [Parameter(Position = 0)]
        [String]
        $Key
    )
    $_result = @{'Cloud-Identity' = '101001'}
    $_full = Get-LicenseProducts
    $_products = $_full.PSObject.Properties.Name | Sort-Object -Unique
    foreach ($_id in $_products) {
        $_friendlyname = $_full.$_id -replace '\W+','-'
        $_result[$_friendlyname] = $_id
    }
    if ($Key) {
        $_result[$Key]
    }
    else {
        $_result
    }
}

function Move-GSChromeOSDevice {
    <#
    .SYNOPSIS
    Moves a ChromeOS device to a new OrgUnit
 
    .DESCRIPTION
    Moves a ChromeOS device to a new OrgUnit
 
    .PARAMETER ResourceId
    The unique ID of the device you would like to move.
 
    .PARAMETER OrgUnitPath
    Full path of the target organizational unit or its ID
 
    .EXAMPLE
    Move-GSChromeOSDevice -ResourceId 'AFiQxQ8Qgd-rouSmcd2UnuvhYV__WXdacTgJhPEA1QoQJrK1hYbKJXm-8JFlhZOjBF4aVbhleS2FVQk5lI069K2GULpteTlLVpKLJFSLSL' -OrgUnitPath '/Corp'
 
    Moves the ChromeOS device specified to the /Corp OrgUnit
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.ChromeOSDevice')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id','Device','DeviceId')]
        [String[]]
        $ResourceId,
        [parameter(Mandatory = $true,Position = 1)]
        [String]
        $OrgUnitPath
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.device.chromeos'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams -Verbose:$false
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            "my_customer"
        }
        $toMove = New-Object 'System.Collections.Generic.List[string]'
    }
    Process {
        $ResourceId | ForEach-Object {
            $toMove.Add($_)
        }
    }
    End {
        try {
            Write-Verbose "Moving the following Chrome OS Devices to OrgUnitPath '$OrgUnitPath':`n - $($toMove -join "`n - ")"
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.ChromeOsMoveDevicesToOu' -Property @{
                DeviceIds = $toMove
            }
            $request = $service.Chromeosdevices.MoveDevicesToOu($body,$customerId,$OrgUnitPath)
            $request.Execute()
            Write-Verbose "The Chrome OS devices were successfully moved."
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

function New-MimeMessage {
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $To,
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $From,
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Subject,
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Body,
        [parameter(Mandatory = $false)]
        [string[]]
        $CC,
        [parameter(Mandatory = $false)]
        [string[]]
        $BCC,
        [parameter(Mandatory = $false)]
        [ValidateScript( {Test-Path $_})]
        [string[]]
        $Attachment,
        [parameter(Mandatory = $false)]
        [switch]
        $BodyAsHtml,
        [parameter(Mandatory = $false)]
        [switch]
        $ReturnConstructedMessage
    )
    $message = [MimeKit.MimeMessage]::new()
    $message.From.Add($From)
    $message.Subject = $Subject
    foreach ($T in $To) {
        $message.To.Add($T)
    }
    if ($CC) {
        foreach ($C in $CC) {
            $message.Cc.Add($C)
        }
    }
    if ($BCC) {
        foreach ($B in $BCC) {
            $message.Bcc.Add($B)
        }
    }
    if ($BodyAsHtml) {
        $TextPart = [MimeKit.TextPart]::new("html")
    }
    else {
        $TextPart = [MimeKit.TextPart]::new("plain")
    }
    $TextPart.Text = $Body
    if ($Attachment) {
        [System.Reflection.Assembly]::LoadWithPartialName('System.IO') | Out-Null
        $Multipart = [MimeKit.Multipart]::new("mixed")
        $Multipart.Add($TextPart)
        foreach ($Attach in $Attachment) {
            $MimeType = (Get-MimeType -File $Attach) -split "/"
            $MimePart = [MimeKit.MimePart]::new($MimeType[0], $MimeType[1])
            $MimePart.ContentObject = [MimeKit.ContentObject]::new([IO.File]::OpenRead($Attach), [MimeKit.ContentEncoding]::Default)
            $MimePart.ContentDisposition = [MimeKit.ContentDisposition]::new([MimeKit.ContentDisposition]::Attachment)
            $MimePart.ContentTransferEncoding = [MimeKit.ContentEncoding]::Base64
            $MimePart.FileName = [IO.Path]::GetFileName($Attach)
            $Multipart.Add($MimePart)
        }
        $message.Body = $Multipart
    }
    else {
        $message.Body = $TextPart
    }
    if ($ReturnConstructedMessage) {
        return $message.ToString()
    }
    else {
        return $message
    }
}
function Out-TruncatedString {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $String,

        [Parameter(Position = 1)]
        [ValidateRange(0,[int32]::MaxValue)]
        [int] $Length = 0
    )

    $outString = $String

    if ($Length -gt 0) {
        if ($String.Length -gt $Length) {
            $outString = $String.Substring(0,($Length - 3)) + '...'
        }
    }

    Write-Output $outString
}
function Read-MimeMessage {
    [cmdletbinding(DefaultParameterSetName = "String")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ParameterSetName = "String")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $String,
        [parameter(Mandatory = $true,ValueFromPipeline = $true,ParameterSetName = "EmlFile")]
        [ValidateScript( {Test-Path $_})]
        [string[]]
        $EmlFile
    )
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            String {
                foreach ($str in $String) {
                    $stream = [System.IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($str))
                    [MimeKit.MimeMessage]::Load($stream)
                    $stream.Dispose()
                }
            }
            EmlFile {
                foreach ($str in $EmlFile) {
                    [MimeKit.MimeMessage]::Load($str)
                }
            }
        }
    }
}
function Read-Prompt {
    [CmdletBinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        $Options,
        [parameter(Mandatory = $false)]
        [String]
        $Title = "Picky Choosy Time",
        [parameter(Mandatory = $false)]
        [String]
        $Message = "Which do you prefer?",
        [parameter(Mandatory = $false)]
        [Int]
        $Default = 0
    )
    Process {
        $opt = @()
        foreach ($option in $Options) {
            switch ($option.GetType().Name) {
                Hashtable {
                    foreach ($key in $option.Keys) {
                        $opt += New-Object System.Management.Automation.Host.ChoiceDescription "$($key)","$($option[$key])"
                    }
                }
                String {
                    $opt += New-Object System.Management.Automation.Host.ChoiceDescription "$option",$null
                }
            }
        }    
        $choices = [System.Management.Automation.Host.ChoiceDescription[]] $opt
        $answer = $host.ui.PromptForChoice($Title, $Message, $choices, $Default)
        $choices[$answer].Label -replace "&"
    }
}
function Resolve-Email {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Ref[]]
        $Email,
        [parameter()]
        [Switch]
        $IsGroup
    )
    Process {
        foreach ($e in $Email) {
            if (-not $IsGroup -and -not ($e.value -as [decimal])) {
                if ($e.value -ceq 'me') {
                    $e.value = $Script:PSGSuite.AdminEmail
                }
                elseif ($e.value -notlike "*@*.*") {
                    $e.value = "$($e.value)@$($Script:PSGSuite.Domain)"
                }
            }
            elseif ($IsGroup -and $e.value -cnotmatch '^([\w.%+-]+@[a-zA-Z\d.-]+\.[a-zA-Z]{2,}|\d{2}[a-z\d]{13})$') {
                $e.value = "$($e.value)@$($Script:PSGSuite.Domain)"
            }
        }
    }
}

function Test-FileLock {
    param (
        [parameter(Mandatory = $false)]
        [string]
        $Path
    )
    if ($Path) {
        $oFile = New-Object System.IO.FileInfo $Path
        if ((Test-Path -Path $Path) -eq $false) {
            return $false
        }
        try {
            $oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
            if ($oStream) {
                $oStream.Close()
            }
            return $false
        }
        catch {
            return $true
        }
    }
}
function ThrowTerm {
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $Message
    )
    New-Object System.Management.Automation.ErrorRecord(
        (New-Object Exception $Message),
        'PowerShell.Module.Error',
        [System.Management.Automation.ErrorCategory]::OperationStopped,
        $null
    )
}
function Write-InlineProgress {
    <#
        .SYNOPSIS
            Display an inline progress bar in the PowerShell console.
        .DESCRIPTION
            Display an inline progress bar in the PowerShell console.
        .NOTES
            Be sure to always call the function with either the -Stop or -Completed switch after the progress bar is finished.
            Be sure to NOT output anything while the progress bar is updating - or it WILL "break"!
            This function will not work when run in PowerShell ISE.
 
            Author: Øyvind Kallstad
            Date: 28.04.2016
            Version: 1.0
        .LINK
            https://communary.wordpress.com/
    #>

    [CmdletBinding(DefaultParameterSetName = 'normal')]
    param (
        # Describe the activity being performed.
        [Parameter(Position = 0, ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [string] $Activity,

        # Minimum padding for the activity text. When set to 0 (default) the size of the activity text
        # is automatically adjusted based on the window width.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [ValidateRange(0,[int]::MaxValue)]
        [int] $ActivityPadding = 0,

        # Display seconds remaining.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [ValidateRange(0,[int]::MaxValue)]
        [int] $SecondsRemaining,

        # Display seconds elapsed.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [ValidateRange(0,[int]::MaxValue)]
        [int] $SecondsElapsed,

        # Define the percent complete value for the progress bar.
        [Parameter(ParameterSetName = 'normal')]
        [ValidateRange(0,100)]
        [int] $PercentComplete,

        # Display the percent complete.
        # Note! If the window width is below 40 the percent value will not be displayed.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [switch] $ShowPercent = $true,

        # Stop without any update to the progress bar.
        [Parameter(ParameterSetName = 'stop')]
        [switch] $Stop,

        # Output last progress result.
        [Parameter(ParameterSetName = 'stop')]
        [switch] $OutputLastProgress,

        # Stop the progress bar with a final update.
        [Parameter(ParameterSetName = 'completed')]
        [switch] $Completed,

        # Customize the progress character.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [ValidateLength(1,1)]
        [ValidateNotNull()]
        [string] $ProgressCharacter = '#',

        # Customize the progress fill character.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [ValidateLength(1,1)]
        [ValidateNotNull()]
        [string] $ProgressFillCharacter = '#',

        # Customize the fill character.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [ValidateLength(1,1)]
        [ValidateNotNull()]
        [string] $ProgressFill = '.',

        # Customize the bracket before the progress bar.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [ValidateLength(0,1)]
        [string] $BarBracketStart = '[',

        # Customize the bracket after the progress bar.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [ValidateLength(0,1)]
        [string] $BarBracketEnd = ']',

        # Use Write-Output instead of Console.Write
        # If you want to support transcripts, you need to use this parameter on the last update of the
        # progress bar so that it will be written to the transcript file. The same goes for any error handling if
        # you want the last status of the progress bar to be written to the transcript.
        [Parameter(ParameterSetName = 'normal')]
        [Parameter(ParameterSetName = 'completed')]
        [switch] $UseWriteOutput
    )

    # this function only works when run from the console
    if ($Host.Name -notlike '*ISE*') {
        if ($Stop) {
            if ($OutputLastProgress) {
                Write-Host (($script:lastProgressString).ToString()) -NoNewline
            }
            else {
                Remove-Variable -Name 'lastProgressString' -Scope 'Script' -ErrorAction SilentlyContinue
                [console]::WriteLine()
            }
            try {
                [System.Console]::CursorVisible = $true
            }
            catch {
                if ($Error[0].Exception.Message -eq 'Exception setting "CursorVisible": "The handle is invalid."') {
                    $Global:Error.Remove($Global:Error[0])
                }
            }
        }
        else {
            if ($Completed) {
                $PercentComplete = 100
                if ($PSBoundParameters.ContainsKey('SecondsRemaining')) {
                    # have to force it to 0 or it will display the last value before it finished
                    $SecondsRemaining = 0
                }
            }

            # if the buffer if full, we need to resize it to make sure that the progress bar don't break
            if (($host.UI.RawUI.CursorPosition.y + 1) -ge ($host.UI.RawUI.BufferSize.Height)) {
                $size = New-Object System.Management.Automation.Host.Size(($host.UI.RawUI.BufferSize.Width), (($host.UI.RawUI.BufferSize.Height + 1000)))
                $host.UI.RawUI.BufferSize = $size
            }

            $cursorPosition = $host.UI.RawUI.CursorPosition
            #$cursorPositionY = $host.UI.RawUI.CursorPosition.Y
            try {
                [System.Console]::CursorVisible = $false
            }
            catch {
                if ($Error[0].Exception.Message -eq 'Exception setting "CursorVisible": "The handle is invalid."') {
                    $Global:Error.Remove($Global:Error[0])
                }
            }
            $windowWidth = [console]::WindowWidth

            # if screen is very small, don't display the percent
            if ($windowWidth -le 40) {$ShowPercent = $false}

            # calculate the size of the activity part of the output string
            if ($ActivityPadding -eq 0) {
                $activityPart = [math]::Floor($windowWidth / 4)
            }
            else {
                $activityPart = $ActivityPadding
            }

            # if activity string is longer than the allocated part length, truncate it
            if ($Activity.Length -gt $activityPart) {
                $Activity = Out-TruncatedString -String $Activity -Length $activityPart
            }

            $progressString = New-Object System.Text.StringBuilder -ArgumentList $windowWidth

            # add activity text to the progress string
            [void]$progressString.Append("$($Activity.PadRight($ActivityPart, ' ')) ")

            # add seconds elapsed to the progress string
            if ($PSBoundParameters.ContainsKey('SecondsElapsed')) {
                [void]$progressString.Append([timespan]::FromSeconds($SecondsElapsed).ToString() + ' ')
            }

            # add seconds remaining to the progress string
            if ($PSBoundParameters.ContainsKey('SecondsRemaining')) {
                [void]$progressString.Append([timespan]::FromSeconds($SecondsRemaining).ToString() + ' ')
            }

            # add the start bracket for the progress bar to the progress string
            [void]$progressString.Append($BarBracketStart)

            # calculate the width of the progress bar
            # the 5 is to account for the space of the percent information
            if ($ShowPercent) {
                $progressBarWidth = $windowWidth - (($progressString.Length) + 5)
            }
            else {
                $progressBarWidth = $windowWidth - ($progressString.Length) + 1
            }

            # add one to the progress bar width if no end bracket is used
            if (-not ($BarBracketEnd)) {
                $progressBarWidth++
            }

            # calculate the bar character percentage and how much of the bar is filled and how much is not filled
            $barCharacterInPercent = ($progressBarWidth - 2) / 100
            $barProgressed = [math]::Floor($PercentComplete * $barCharacterInPercent)
            $barNotProgressed = ($progressBarWidth - 2) - $barProgressed

            # add the progress bar to progress string
            if ($barProgressed -gt 0) {
                if ($barNotProgressed -gt 0) {
                    [void]$progressString.Append(($ProgressFillCharacter * ($barProgressed - 1)))
                    [void]$progressString.Append($ProgressCharacter)
                }
                else {
                    [void]$progressString.Append(($ProgressFillCharacter * $barProgressed))
                }
            }
            [void]$progressString.Append("$($ProgressFill * $barNotProgressed)$($BarBracketEnd)")

            # add the percent complete to the progress string
            if ($ShowPercent) {
                [void]$progressString.Append(" $($PercentComplete.ToString().PadLeft(3, ' '))% ")
            }

            # if not already present, create a string builder to hold the last progress string
            if (-not ($script:lastProgressString)) {
                $script:lastProgressString = New-Object System.Text.StringBuilder($windowWidth)
            }

            # only update the progress string if it's different from the last (optimization)
            if (-not($script:lastProgressString.ToString() -eq $progressString.ToString())) {
                if ($UseWriteOutput) {
                    Write-Output ($progressString.ToString())
                }
                else {
                    [console]::Write(($progressString.ToString()))
                }

                $host.UI.RawUI.CursorPosition = $cursorPosition
                #$host.UI.RawUI.CursorPosition = 0, $cursorPositionY
                [void]$script:lastProgressString.Clear()
                [void]$script:lastProgressString.Append($progressString.ToString())
            }

            if ($Completed) {
                # do some clean-up and jump to the next line
                Remove-Variable -Name 'lastProgressString' -Scope 'Script' -ErrorAction SilentlyContinue
                try {
                    [System.Console]::CursorVisible = $true
                }
                catch {
                    if ($Error[0].Exception.Message -eq 'Exception setting "CursorVisible": "The handle is invalid."') {
                        $Global:Error.Remove($Global:Error[0])
                    }
                }
                [console]::WriteLine()
            }
        }
    }
    else {
        Write-Warning 'This function is not compatible with PowerShell ISE.'
    }
}

function Get-GSOrganizationalUnitListPrivate {
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [Alias('OrgUnitPath','BaseOrgUnitPath')]
        [String]
        $SearchBase,
        [parameter(Mandatory = $false)]
        [Alias('Type')]
        [ValidateSet('Subtree','OneLevel','All','Children')]
        [String]
        $SearchScope = 'All'
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.orgunit'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Getting all Organizational Units"
            $request = $service.Orgunits.List($Script:PSGSuite.CustomerId)
            $request.Type = switch ($SearchScope) {
                Subtree {
                    'All'
                }
                OneLevel {
                    'Children'
                }
                default {
                    $SearchScope
                }
            }
            if ($SearchBase) {
                $request.OrgUnitPath = $SearchBase
            }
            $request.Execute() | Select-Object -ExpandProperty OrganizationUnits
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}
function Get-GSResourceListPrivate {
    [CmdletBinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('Calendars','Buildings','Features')]
        [String[]]
        $Resource = @('Calendars','Buildings','Features'),
        [parameter(Mandatory = $false)]
        [Alias('Query')]
        [String[]]
        $Filter,
        [parameter(Mandatory = $false)]
        [String[]]
        $OrderBy,
        [parameter(Mandatory = $false)]
        [ValidateRange(1,500)]
        [Alias("MaxResults")]
        [Int]
        $PageSize = "500"
    )
    Begin {
        if ($MyInvocation.InvocationName -eq 'Get-GSCalendarResourceList') {
            $Resource = 'Calendars'
        }
        $propHash = @{
            Calendars  = 'Items'
            Buildings = 'BuildingsValue'
            Features = 'FeaturesValue'
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.resource.calendar'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($R in $Resource) {
                $request = $service.Resources.$R.List($(if($Script:PSGSuite.CustomerID) {$Script:PSGSuite.CustomerID}else {'my_customer'}))
                if ($R -eq 'Calendars') {
                    if ($PageSize) {
                        $request.MaxResults = $PageSize
                    }
                    if ($OrderBy) {
                        $request.OrderBy = "$($OrderBy -join ", ")"
                    }
                    if ($Filter) {
                        if ($Filter -eq '*') {
                            $Filter = ""
                        }
                        else {
                            $Filter = "$($Filter -join " ")"
                        }
                        $Filter = $Filter -replace " -eq ","=" -replace " -like ",":" -replace " -match ",":" -replace " -contains ",":" -creplace "'True'","True" -creplace "'False'","False"
                        $request.Query = $Filter.Trim()
                        Write-Verbose "Getting Resource $R matching filter: `"$($Filter.Trim())`""
                    }
                    else {
                        Write-Verbose "Getting all Resource $R"
                    }
                }
                else {
                    Write-Verbose "Getting all Resource $R"
                }
                [int]$i = 1
                do {
                    $result = $request.Execute()
                    $result.$($propHash[$R]) | ForEach-Object {
                        $obj = $_
                        $_Id = switch ($R) {
                            Calendars {
                                $obj.ResourceId
                            }
                            Buildings {
                                $obj.BuildingId
                            }
                            Features {
                                $obj.Name
                            }
                        }
                        $_ | Add-Member -MemberType NoteProperty -Name 'Id' -Value $_Id -PassThru | Add-Member -MemberType NoteProperty -Name 'Resource' -Value $R -PassThru | Add-Member -MemberType ScriptMethod -Name ToString -Value {$this.Id} -PassThru -Force
                    }
                    if ($result.NextPageToken) {
                        $request.PageToken = $result.NextPageToken
                    }
                    [int]$retrieved = ($i + $result.$($propHash[$R]).Count) - 1
                    Write-Verbose "Retrieved $retrieved resources..."
                    [int]$i = $i + $result.$($propHash[$R]).Count
                }
                until (!$result.NextPageToken)
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}
function Get-GSShortUrlListPrivate {
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","Email")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory=$false)]
        [ValidateSet("Full","Analytics_Clicks")]
        [string]
        $Projection = "Full"
    )
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/urlshortener'
                ServiceType = 'Google.Apis.Urlshortener.v1.UrlshortenerService'
                User        = "$U"
            }
            $service = New-GoogleService @serviceParams
            try {
                Write-Verbose "Getting Short Url list for User '$U'"
                $request = $service.Url.List()
                $result = $request.Execute()
                if ($null -ne $result.Items) {
                    $result.Items | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

function Get-GSUserASPListPrivate {
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($U in $User) {
            try {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Getting ASP list for User '$U'"
                $request = $service.Asps.List($U)
                $result = $request.Execute()
                if ($null -ne $result.Items) {
                    $result.Items | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}
function Get-GSUserSchemaListPrivate {
    [cmdletbinding()]
    Param( )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.userschema'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            $request = $service.Schemas.List($Script:PSGSuite.CustomerId)
            $result = $request.Execute()
            if ($null -ne $result.SchemasValue) {
                $result.SchemasValue
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}
function Get-GSUserTokenListPrivate {
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($U in $User) {
            try {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Getting Token list for User '$U'"
                $request = $service.Tokens.List($U)
                $result = $request.Execute()
                if ($null -ne $result.Items) {
                    $result.Items | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}
function Clear-PSGSuiteServiceCache {
    <#
    .SYNOPSIS
    Clears the dictionary of cached service objects created with New-GoogleService.
 
    .DESCRIPTION
    Clears the dictionary of cached service objects created with New-GoogleService.
 
    .EXAMPLE
    Clear-PSGSuiteServiceCache
 
    .LINK
    https://psgsuite.io/Function%20Help/Authentication/Clear-PSGSuiteServiceCache/
    #>

    [CmdletBinding()]
    Param ()
    Begin{
        if (-not $script:_PSGSuiteSessions) {
            $script:_PSGSuiteSessions = @{}
        }
        $toRemove = @()
    }
    Process {
        if (-not $script:_PSGSuiteSessions.Keys.Count) {
            Write-Verbose "There are no current cached sessions to clear!"
        }
        else {
            foreach ($key in $script:_PSGSuiteSessions.Keys) {
                Write-Verbose "Clearing cached session with key: $key"
                $script:_PSGSuiteSessions[$key].Service.Dispose()
                $toRemove += $key
            }
        }
    }
    End {
        foreach ($key in $toRemove) {
            $script:_PSGSuiteSessions.Remove($key)
        }
    }
}

Export-ModuleMember -Function 'Clear-PSGSuiteServiceCache'
function Get-GSToken {
    <#
    .SYNOPSIS
    Requests an Access Token for REST API authentication. Defaults to 3600 seconds token expiration time.
 
    .DESCRIPTION
    Requests an Access Token for REST API authentication. Defaults to 3600 seconds token expiration time.
 
    .PARAMETER Scopes
    The list of scopes to request the token for
 
    .PARAMETER AdminEmail
    The email address of the user to request the token for. This is typically the Admin user.
 
    .EXAMPLE
    $Token = Get-GSToken -Scopes 'https://www.google.com/m8/feeds' -AdminEmail $User
    $headers = @{
        Authorization = "Bearer $($Token)"
        'GData-Version' = '3.0'
    }
 
    .LINK
    https://psgsuite.io/Function%20Help/Authentication/Get-GSToken/
    #>

    Param (
        [parameter(Mandatory = $true)]
        [Alias('Scope')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Scopes,
        [parameter(Mandatory = $false)]
        [Alias('User')]
        [ValidateNotNullOrEmpty()]
        [String]
        $AdminEmail = $Script:PSGSuite.AdminEmail
    )
    try {
        Write-Verbose "Acquiring access token"
        $serviceParams = @{
            Scope       = $Scopes
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $AdminEmail
        }
        $service = New-GoogleService @serviceParams
        ($service.HttpClientInitializer.GetAccessTokenForRequestAsync()).Result
    }
    catch {
        Write-Verbose "Failed to acquire access token!"
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

Export-ModuleMember -Function 'Get-GSToken'
function Get-PSGSuiteServiceCache {
    <#
    .SYNOPSIS
    Returns the dictionary of cached service objects created with New-GoogleService for inspection.
 
    .DESCRIPTION
    Returns the dictionary of cached service objects created with New-GoogleService for inspection.
 
    The keys in the session cache dictionary are comprised of the following values which are added to the cache whenever a new session is created:
 
        $SessionKey = @($User,$ServiceType,$(($Scope | Sort-Object) -join ";")) -join ";"
 
    .PARAMETER IncludeKeys
    If $true, returns the full service cache dictionary including keys.
 
    Defaults to $false.
 
    .EXAMPLE
    Get-PSGSuiteServiceCache
 
    .LINK
    https://psgsuite.io/Function%20Help/Authentication/Get-PSGSuiteServiceCache/
    #>

    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $false,Position = 0)]
        [Switch]
        $IncludeKeys
    )
    Begin{
        if (-not $script:_PSGSuiteSessions) {
            $script:_PSGSuiteSessions = @{}
        }
    }
    Process {
        Write-Verbose "Getting cached session list"
        if ($IncludeKeys) {
            $script:_PSGSuiteSessions
        }
        else {
            $script:_PSGSuiteSessions.Values
        }
    }
}

Export-ModuleMember -Function 'Get-PSGSuiteServiceCache'
function New-GoogleService {
    <#
    .SYNOPSIS
    Creates a new Google Service object that handles authentication for the scopes specified
 
    .DESCRIPTION
    Creates a new Google Service object that handles authentication for the scopes specified
 
    .PARAMETER Scope
    The scope or scopes to build the service with, e.g. https://www.googleapis.com/auth/admin.reports.audit.readonly
 
    .PARAMETER ServiceType
    The type of service to create, e.g. Google.Apis.Admin.Reports.reports_v1.ReportsService
 
    .PARAMETER User
    The user to request the service for during the authentication process
 
    .EXAMPLE
    $serviceParams = @{
        Scope = 'https://www.googleapis.com/auth/admin.reports.audit.readonly'
        ServiceType = 'Google.Apis.Admin.Reports.reports_v1.ReportsService'
    }
    $service = New-GoogleService @serviceParams
 
    .LINK
    https://psgsuite.io/Function%20Help/Authentication/New-GoogleService/
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true,Position = 0)]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Scope,
        [Parameter(Mandatory = $true,Position = 1)]
        [String]
        $ServiceType,
        [Parameter(Mandatory = $false,Position = 2)]
        [Alias('AdminEmail')]
        [String]
        $User = $script:PSGSuite.AdminEmail
    )
    Begin {
        if (-not $script:_PSGSuiteSessions) {
            $script:_PSGSuiteSessions = @{}
        }
        $sessionKey = @($User,$ServiceType,$(($Scope | Sort-Object) -join ";")) -join ";"
    }
    Process {
        if ($script:_PSGSuiteSessions.ContainsKey($sessionKey)) {
            if (-not $script:_PSGSuiteSessions[$sessionKey].Acknowledged) {
                Write-Verbose "Using matching cached service for user '$User'"
                $script:_PSGSuiteSessions[$sessionKey].Acknowledged = $true
            }
            $script:_PSGSuiteSessions[$sessionKey].LastUsed = Get-Date
            $script:_PSGSuiteSessions[$sessionKey] | Select-Object -ExpandProperty Service
        }
        else {
            if ($script:PSGSuite.JSONServiceAccountKey -or $script:PSGSuite.JSONServiceAccountKeyPath) {
                Write-Verbose "Building ServiceAccountCredential from JSONServiceAccountKey as user '$User'"
                try {
                    if (-not $script:PSGSuite.JSONServiceAccountKey) {
                        $script:PSGSuite.JSONServiceAccountKey = ([System.IO.File]::ReadAllBytes($script:PSGSuite.JSONServiceAccountKeyPath))
                        Set-PSGSuiteConfig -ConfigName $script:PSGSuite.ConfigName -JSONServiceAccountKey $script:PSGSuite.JSONServiceAccountKey -Verbose:$false
                    }
                    $stream = New-Object System.IO.MemoryStream $([System.Text.Encoding]::ASCII.GetBytes($script:PSGSuite.JSONServiceAccountKey)), $null
                    $credential = ([Google.Apis.Auth.OAuth2.GoogleCredential]::FromStream($stream)).CreateWithUser($User).CreateScoped($Scope).UnderlyingCredential
                }
                catch {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                finally {
                    if ($stream) {
                        $stream.Close()
                    }
                }
            }
            elseif ($script:PSGSuite.P12KeyPath -or $script:PSGSuite.P12Key -or $script:PSGSuite.P12KeyObject) {
                try {
                    Write-Verbose "Building ServiceAccountCredential from P12Key as user '$User'"
                    if ($script:PSGSuite.P12KeyPath -or $script:PSGSuite.P12Key) {
                        if (-not $script:PSGSuite.P12Key) {
                            $script:PSGSuite.P12Key = ([System.IO.File]::ReadAllBytes($script:PSGSuite.P12KeyPath))
                            Set-PSGSuiteConfig -ConfigName $script:PSGSuite.ConfigName -P12Key $script:PSGSuite.P12Key -Verbose:$false
                        }
                        if ($script:PSGSuite.P12KeyPassword) {
                            $P12KeyPassword = $script:PSGSuite.P12KeyPassword
                        }
                        else {
                            $P12KeyPassword = "notasecret"
                        }
                        $certificate = New-Object 'System.Security.Cryptography.X509Certificates.X509Certificate2' -ArgumentList ([System.Byte[]]$script:PSGSuite.P12Key),$P12KeyPassword,([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
                    }
                    else {
                        $certificate = $script:PSGSuite.P12KeyObject
                    }
                    $credential = New-Object 'Google.Apis.Auth.OAuth2.ServiceAccountCredential' (New-Object 'Google.Apis.Auth.OAuth2.ServiceAccountCredential+Initializer' $script:PSGSuite.AppEmail -Property @{
                            User   = $User
                            Scopes = [string[]]$Scope
                        }
                    ).FromCertificate($certificate)

                }
                catch {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            }
            elseif ($script:PSGSuite.ClientSecretsPath -or $script:PSGSuite.ClientSecrets) {
                try {
                    $ClientSecretsScopes = @(
                        'https://www.google.com/m8/feeds'
                        'https://mail.google.com'
                        'https://www.googleapis.com/auth/gmail.settings.basic'
                        'https://www.googleapis.com/auth/gmail.settings.sharing'
                        'https://www.googleapis.com/auth/calendar'
                        'https://www.googleapis.com/auth/drive'
                        'https://www.googleapis.com/auth/tasks'
                        'https://www.googleapis.com/auth/tasks.readonly'
                    )
                    if (-not $script:PSGSuite.ClientSecrets) {
                        $script:PSGSuite.ClientSecrets = ([System.IO.File]::ReadAllText($script:PSGSuite.ClientSecretsPath))
                        Set-PSGSuiteConfig -ConfigName $script:PSGSuite.ConfigName -ClientSecrets $script:PSGSuite.ClientSecrets -Verbose:$false
                    }
                    $credPath = Join-Path (Resolve-Path (Join-Path "~" ".scrthq")) "PSGSuite"
                    Write-Verbose "Building UserCredentials from ClientSecrets as user '$User' and prompting for authorization if necessary."
                    $stream = New-Object System.IO.MemoryStream $([System.Text.Encoding]::ASCII.GetBytes(($script:PSGSuite.ClientSecrets))),$null
                    $credential = [Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker]::AuthorizeAsync(
                        [Google.Apis.Auth.OAuth2.GoogleClientSecrets]::Load($stream).Secrets,
                        [string[]]$ClientSecretsScopes,
                        $User,
                        [System.Threading.CancellationToken]::None,
                        $(New-Object 'Google.Apis.Util.Store.FileDataStore' -ArgumentList $credPath,$true),
                        $(if ($PSVersionTable.PSVersion.Major -gt 5) {
                                New-Object 'Google.Apis.Auth.OAuth2.PromptCodeReceiver'
                            }
                            else {
                                New-Object 'Google.Apis.Auth.OAuth2.LocalServerCodeReceiver'
                            })
                    ).Result
                }
                catch {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                finally {
                    if ($stream) {
                        $stream.Close()
                    }
                }
            }
            else {
                $PSCmdlet.ThrowTerminatingError((ThrowTerm "The current config '$($script:PSGSuite.ConfigName)' does not contain a JSONServiceAccountKeyPath, P12KeyPath, or ClientSecretsPath! PSGSuite is unable to build a credential object for the service without a path to a credential file! Please update the configuration to include a path at least one of the three credential types."))
            }
            $svc = New-Object "$ServiceType" (New-Object 'Google.Apis.Services.BaseClientService+Initializer' -Property @{
                    HttpClientInitializer = $credential
                    ApplicationName       = "PSGSuite"
                }
            )
            $script:_PSGSuiteSessions[$sessionKey] = ([PSCustomObject]@{
                User         = $User
                Scope        = $Scope
                Service      = $svc
                Issued       = Get-Date
                LastUsed     = Get-Date
                Acknowledged = $false
            })
            return $svc
        }
    }
}

Export-ModuleMember -Function 'New-GoogleService'
function Add-GSCalendarSubscription {
    <#
    .SYNOPSIS
    Adds a calendar to a users calendar list (aka subscribes to the specified calendar)
 
    .DESCRIPTION
    Adds a calendar to a users calendar list (aka subscribes to the specified calendar)
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    .PARAMETER CalendarID
    The calendar ID of the calendar you would like to subscribe the user to
 
    .PARAMETER ColorRgbFormat
    Whether to use the foregroundColor and backgroundColor fields to write the calendar colors (RGB). If this feature is used, the index-based colorId field will be set to the best matching option automatically.
 
    .PARAMETER Selected
    Whether the calendar content shows up in the calendar UI. Optional. The default is False.
 
    .PARAMETER Hidden
    Whether the calendar has been hidden from the list. Optional. The default is False.
 
    .PARAMETER Reminders
    A list of reminders to add to this calendar.
 
    This parameter expects a 'Google.Apis.Calendar.v3.Data.EventReminder[]' object type. You can create objects of this type easily by using the function 'Add-GSCalendarEventReminder'
 
    .PARAMETER DefaultReminderMethod
    The method used by this reminder. Defaults to email.
 
    Possible values are:
    * "email" - Reminders are sent via email.
    * "sms" - Reminders are sent via SMS. These are only available for G Suite customers. Requests to set SMS reminders for other account types are ignored.
    * "popup" - Reminders are sent via a UI popup.
 
    .PARAMETER DefaultReminderMinutes
    Number of minutes before the start of the event when the reminder should trigger. Defaults to 30 minutes.
 
    Valid values are between 0 and 40320 (4 weeks in minutes).
 
    .PARAMETER Notifications
    A list of notifications to add to this calendar.
 
    This parameter expects a 'Google.Apis.Calendar.v3.Data.CalendarNotification[]' object type. You can create objects of this type easily by using the function 'Add-GSCalendarNotification'
 
    .PARAMETER DefaultNotificationMethod
    The method used to deliver the notification.
 
    Possible values are:
    * "email" - Reminders are sent via email.
    * "sms" - Reminders are sent via SMS. This value is read-only and is ignored on inserts and updates. SMS reminders are only available for G Suite customers.
 
    .PARAMETER DefaultNotificationType
    The type of notification.
 
    Possible values are:
    * "eventCreation" - Notification sent when a new event is put on the calendar.
    * "eventChange" - Notification sent when an event is changed.
    * "eventCancellation" - Notification sent when an event is cancelled.
    * "eventResponse" - Notification sent when an event is changed.
    * "agenda" - An agenda with the events of the day (sent out in the morning).
 
    .PARAMETER Color
    The color of the calendar.
 
    .PARAMETER SummaryOverride
    The summary that the authenticated user has set for this calendar.
 
    .EXAMPLE
    Add-GSCalendarSubscription -User me -CalendarId john.smith@domain.com -Selected -Color Cyan
 
    Adds the calendar 'john.smith@domain.com' to the AdminEmail user's calendar list
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/Add-GSCalendarSubscription/
 
    .LINK
    https://developers.google.com/calendar/v3/reference/calendarList/insert
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.CalendarListEntry')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory,Position = 1)]
        [String[]]
        $CalendarId,
        [parameter(Mandatory = $false)]
        [Switch]
        $ColorRgbFormat,
        [parameter()]
        [Switch]
        $Selected,
        [parameter()]
        [Switch]
        $Hidden,
        [parameter()]
        [Google.Apis.Calendar.v3.Data.EventReminder[]]
        $Reminders,
        [parameter()]
        [ValidateSet('email','sms','popup')]
        [String]
        $DefaultReminderMethod,
        [parameter()]
        [ValidateRange(0,40320)]
        [Int]
        $DefaultReminderMinutes = 30,
        [parameter()]
        [Google.Apis.Calendar.v3.Data.CalendarNotification[]]
        $Notifications,
        [parameter()]
        [ValidateSet('email','sms')]
        [String]
        $DefaultNotificationMethod,
        [parameter()]
        [ValidateSet('eventCreation','eventChange','eventCancellation','eventResponse','agenda')]
        [String]
        $DefaultNotificationType,
        [parameter()]
        [ValidateSet("Periwinkle","Seafoam","Lavender","Coral","Goldenrod","Beige","Cyan","Grey","Blue","Green","Red")]
        [String]
        $Color,
        [parameter()]
        [String]
        $SummaryOverride
    )
    Begin {
        $colorHash = @{
            Periwinkle = 1
            Seafoam    = 2
            Lavender   = 3
            Coral      = 4
            Goldenrod  = 5
            Beige      = 6
            Cyan       = 7
            Grey       = 8
            Blue       = 9
            Green      = 10
            Red        = 11
        }
    }
    Process {
        try {
            if ($User -ceq 'me') {
                $User = $Script:PSGSuite.AdminEmail
            }
            elseif ($User -notlike "*@*.*") {
                $User = "$($User)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/calendar'
                ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                User        = $User
            }
            $service = New-GoogleService @serviceParams
            foreach ($calId in $CalendarID) {
                $body = New-Object 'Google.Apis.Calendar.v3.Data.CalendarListEntry' -Property @{
                    Id = $calId
                    Selected = $Selected
                    Hidden = $Hidden
                }
                if ($PSBoundParameters.ContainsKey('Reminders')) {
                    $body.DefaultReminders = $Reminders
                }
                elseif ($PSBoundParameters.ContainsKey('DefaultReminderMethod')) {
                    $DefaultReminders = New-Object 'Google.Apis.Calendar.v3.Data.EventReminder' -Property @{
                        Method = $DefaultReminderMethod
                        Minutes = $DefaultReminderMinutes
                    }
                    $body.DefaultReminders = [Google.Apis.Calendar.v3.Data.EventReminder[]]$DefaultReminders
                }
                if ($PSBoundParameters.ContainsKey('Notifications')) {
                    $body.NotificationSettings = New-Object 'Google.Apis.Calendar.v3.Data.CalendarListEntry+NotificationSettingsData' -Property @{
                        Notifications = [Google.Apis.Calendar.v3.Data.CalendarNotification[]]$Notifications
                    }
                }
                elseif ($PSBoundParameters.ContainsKey('DefaultNotificationMethod') -or $PSBoundParameters.ContainsKey('DefaultNotificationType')) {
                    $DefaultNotification = New-Object 'Google.Apis.Calendar.v3.Data.CalendarNotification'
                    if ($PSBoundParameters.ContainsKey('DefaultNotificationMethod')) {
                        $DefaultNotification.Method = $DefaultNotificationMethod
                    }
                    if ($PSBoundParameters.ContainsKey('DefaultNotificationType')) {
                        $DefaultNotification.Type = $DefaultNotificationType
                    }
                    $body.NotificationSettings = New-Object 'Google.Apis.Calendar.v3.Data.CalendarListEntry+NotificationSettingsData' -Property @{
                        Notifications = [Google.Apis.Calendar.v3.Data.CalendarNotification[]]$DefaultNotification
                    }
                }
                foreach ($key in $PSBoundParameters.Keys) {
                    switch ($key) {
                        Color {
                            $body.ColorId = $colorHash[$Color]
                        }
                        Default {
                            if ($body.PSObject.Properties.Name -contains $key) {
                                $body.$key = $PSBoundParameters[$key]
                            }
                        }
                    }
                }
                Write-Verbose "Subscribing user '$User' to Calendar '$($calId)'"
                $request = $service.CalendarList.Insert($body)
                if ($PSBoundParameters.ContainsKey('ColorRgbFormat')) {
                    $request.ColorRgbFormat = $ColorRgbFormat
                }
                $request.Execute()
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSCalendarSubscription'
function Get-GSCalendar {
    <#
    .SYNOPSIS
    Returns metadata for a calendar. If no calendar ID is specified, returns the list of calendar subscriptions via Get-GSCalendarSubscription
 
    .DESCRIPTION
    Returns metadata for a calendar. If no calendar ID is specified, returns the list of calendar subscriptions via Get-GSCalendarSubscription
 
    .PARAMETER CalendarId
    The Id of the calendar you would like to get.
 
    If excluded, returns the list of calendars for the user.
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .PARAMETER MinAccessRole
    The minimum access role for the user in the returned entries. Optional. The default is no restriction.
 
    .PARAMETER PageSize
    Maximum number of entries returned on one result page. The page size can never be larger than 250 entries.
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .PARAMETER ShowDeleted
    Whether to include deleted calendar list entries in the result. Optional. The default is False.
 
    .PARAMETER ShowHidden
    Whether to show hidden entries. Optional. The default is False.
 
    .PARAMETER SyncToken
    Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then.
 
    If only read-only fields such as calendar properties or ACLs have changed, the entry won't be returned. All entries deleted and hidden since the previous list request will always be in the result set and it is not allowed to set showDeleted neither showHidden to False.
 
    To ensure client state consistency minAccessRole query parameter cannot be specified together with nextSyncToken. If the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken. Learn more about incremental synchronization.
 
    Optional. The default is to return all entries.
 
    .EXAMPLE
    Get-GSCalendar
 
    Gets the list of calendar subscriptions for the AdminEmail user.
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/Get-GSCalendar/
 
    .LINK
    https://developers.google.com/calendar/v3/reference/calendars/get
 
    .LINK
    https://developers.google.com/calendar/v3/reference/calendarList/list
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.Calendar')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param (
        [parameter(Mandatory,Position = 0,ValueFromPipelineByPropertyName,ParameterSetName = "Get")]
        [Alias('Id')]
        [String[]]
        $CalendarId,
        [parameter(ValueFromPipelineByPropertyName)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Google.Apis.Calendar.v3.CalendarListResource+ListRequest+MinAccessRoleEnum]
        $MinAccessRole,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('MaxResults')]
        [ValidateRange(1,250)]
        [Int]
        $PageSize = 250,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [switch]
        $ShowDeleted,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [switch]
        $ShowHidden,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [String]
        $SyncToken
    )
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($U in $User) {
                    if ($U -ceq 'me') {
                        $U = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($U -notlike "*@*.*") {
                        $U = "$($U)@$($Script:PSGSuite.Domain)"
                    }
                    $serviceParams = @{
                        Scope       = 'https://www.googleapis.com/auth/calendar'
                        ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                        User        = $U
                    }
                    $service = New-GoogleService @serviceParams
                    foreach ($calId in $CalendarId) {
                        try {
                            $request = $service.Calendars.Get($calId)
                            Write-Verbose "Getting Calendar Id '$calId' for User '$U'"
                            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                        }
                        catch {
                            if ($ErrorActionPreference -eq 'Stop') {
                                $PSCmdlet.ThrowTerminatingError($_)
                            }
                            else {
                                Write-Error $_
                            }
                        }
                    }
                }
            }
            List {
                Get-GSCalendarSubscription @PSBoundParameters
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCalendar'
function Get-GSCalendarAcl {
    <#
    .SYNOPSIS
    Gets the Access Control List for a calendar
 
    .DESCRIPTION
    Gets the Access Control List for a calendar
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .PARAMETER CalendarId
    The calendar ID of the calendar you would like to list ACLS for.
 
    Defaults to the user's primary calendar
 
    .PARAMETER RuleId
    The Id of the Rule you would like to retrieve specifically. Leave empty to return the full ACL list instead.
 
    .PARAMETER PageSize
    Maximum number of events returned on one result page.
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSCalendarACL -User me -CalendarID "primary"
 
    This gets the ACL on the primary calendar of the AdminUser.
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/Get-GSCalendarACL/
 
    .LINK
    https://developers.google.com/calendar/v3/reference/acl/get
 
    .LINK
    https://developers.google.com/calendar/v3/reference/acl/list
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.AclRule')]
    [cmdletbinding(DefaultParameterSetName = 'List')]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $CalendarId = "primary",
        [parameter(Mandatory = $false, ParameterSetName = 'Get')]
        [String]
        $RuleId,
        [parameter(Mandatory = $false, ParameterSetName = 'List')]
        [ValidateRange(1,2500)]
        [Int]
        $PageSize = 2500,
        [parameter(Mandatory = $false, ParameterSetName = 'List')]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/calendar'
                ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                User        = $U
            }
            $service = New-GoogleService @serviceParams
            foreach ($calId in $CalendarId) {
                try {
                    switch ($PSCmdlet.ParameterSetName) {
                        Get {
                            Write-Verbose "Getting ACL Id '$RuleId' of calendar '$calId' for user '$U'"
                            $request = $service.Acl.Get($calId,$RuleId)
                            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru | Add-Member -MemberType NoteProperty -Name 'CalendarId' -Value $calId -PassThru
                        }
                        List {
                            $request = $service.Acl.List($calId)
                            foreach ($key in $PSBoundParameters.Keys | Where-Object {$_ -ne 'CalendarId'}) {
                                switch ($key) {
                                    Default {
                                        if ($request.PSObject.Properties.Name -contains $key) {
                                            $request.$key = $PSBoundParameters[$key]
                                        }
                                    }
                                }
                            }
                            if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                                $PageSize = $Limit
                            }
                            $request.MaxResults = $PageSize
                            Write-Verbose "Getting ACL List of calendar '$calId' for user '$U'"
                            [int]$i = 1
                            $overLimit = $false
                            do {
                                $result = $request.Execute()
                                $result.Items | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru | Add-Member -MemberType NoteProperty -Name 'CalendarId' -Value $calId -PassThru
                                if ($result.NextPageToken) {
                                    $request.PageToken = $result.NextPageToken
                                }
                                [int]$retrieved = ($i + $result.Items.Count) - 1
                                Write-Verbose "Retrieved $retrieved Calendar ACLs..."
                                if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                                    Write-Verbose "Limit reached: $Limit"
                                    $overLimit = $true
                                }
                                elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                                    $newPS = $Limit - $retrieved
                                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                                    $request.MaxResults = $newPS
                                }
                                [int]$i = $i + $result.Items.Count
                            }
                            until ($overLimit -or !$result.NextPageToken)
                        }
                    }
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCalendarACL'
function Get-GSCalendarEvent {
    <#
    .SYNOPSIS
    Gets the calendar events for a user
 
    .DESCRIPTION
    Gets the calendar events for a user
 
    .PARAMETER EventId
    The Id of the event to get info for
 
    .PARAMETER CalendarId
    The calendar ID of the calendar you would like to list events from.
 
    Defaults to the user's primary calendar
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .PARAMETER Filter
    Free text search terms to find events that match these terms in any field, except for extended properties.
 
    .PARAMETER OrderBy
    The order of the events returned in the result.
 
    Acceptable values are:
    * "startTime": Order by the start date/time (ascending). This is only available when querying single events (i.e. the parameter singleEvents is True)
    * "updated": Order by last modification time (ascending).
 
    .PARAMETER MaxAttendees
    The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned.
 
    .PARAMETER PageSize
    Maximum number of events returned on one result page.
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .PARAMETER ShowDeleted
    Whether to include deleted events (with status equals "cancelled") in the result. Cancelled instances of recurring events (but not the underlying recurring event) will still be included if showDeleted and singleEvents are both False. If showDeleted and singleEvents are both True, only single instances of deleted events (but not the underlying recurring events) are returned.
 
    .PARAMETER ShowHiddenInvitations
    Whether to include hidden invitations in the result.
 
    .PARAMETER SingleEvents
    Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves.
 
    .PARAMETER PrivateExtendedProperty
    Extended properties constraint specified as a hashtable where propertyName=value. Matches only private properties.
 
    .PARAMETER SharedExtendedProperty
    Extended properties constraint specified as a hashtable where propertyName=value. Matches only shared properties.
 
    .PARAMETER TimeMin
    Lower bound (inclusive) for an event's end time to filter by. If TimeMax is set, TimeMin must be smaller than timeMax.
 
    .PARAMETER TimeMax
    Upper bound (exclusive) for an event's start time to filter by. If TimeMin is set, TimeMax must be greater than timeMin.
 
    .EXAMPLE
    Get-GSCalendarEventList -TimeMin (Get-Date "01-21-2018 00:00:00") -TimeMax (Get-Date "01-28-2018 23:59:59") -SingleEvents
 
    This gets the single events on the primary calendar of the Admin for the week of Jan 21-28, 2018.
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/Get-GSCalendarEvent/
 
    .LINK
    https://developers.google.com/calendar/v3/reference/events/get
 
    .LINK
    https://developers.google.com/calendar/v3/reference/events/list
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.Event')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ParameterSetName = "Get")]
        [String[]]
        $EventId,
        [parameter(Mandatory = $false)]
        [String]
        $CalendarId = "primary",
        [parameter(Mandatory = $false,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('Q','Query')]
        [String]
        $Filter,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateSet("StartTime","Updated")]
        [String]
        $OrderBy,
        [parameter(Mandatory = $false)]
        [Int]
        $MaxAttendees,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateRange(1,2500)]
        [Int]
        $PageSize = 2500,
        [parameter(Mandatory = $false,ParameterSetName = 'List')]
        [Alias('First')]
        [Int]
        $Limit = 0,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [switch]
        $ShowDeleted,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [switch]
        $ShowHiddenInvitations,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [switch]
        $SingleEvents,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Hashtable]
        $PrivateExtendedProperty,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Hashtable]
        $SharedExtendedProperty,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [DateTime]
        $TimeMin,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [DateTime]
        $TimeMax
    )
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/calendar'
                ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                User        = $U
            }
            $service = New-GoogleService @serviceParams
            foreach ($calId in $CalendarId) {
                switch ($PSCmdlet.ParameterSetName) {
                    Get {
                        foreach ($evId in $EventId) {
                            try {
                                $request = $service.Events.Get($calId,$evId)
                                if ($PSBoundParameters.Keys -contains 'MaxAttendees') {
                                    $request.MaxAttendees = $MaxAttendees
                                }
                                Write-Verbose "Getting Event ID '$evId' from Calendar '$calId' for User '$U'"
                                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru | Add-Member -MemberType NoteProperty -Name 'CalendarId' -Value $calId -PassThru
                            }
                            catch {
                                if ($ErrorActionPreference -eq 'Stop') {
                                    $PSCmdlet.ThrowTerminatingError($_)
                                }
                                else {
                                    Write-Error $_
                                }
                            }
                        }
                    }
                    List {
                        try {
                            $request = $service.Events.List($calId)
                            foreach ($key in $PSBoundParameters.Keys | Where-Object {$_ -ne 'CalendarId'}) {
                                switch ($key) {
                                    Filter {
                                        $request.Q = $Filter
                                    }
                                    PrivateExtendedProperty {
                                        $converted = $PrivateExtendedProperty.Keys | Foreach-Object {
                                            "$($_)=$($PrivateExtendedProperty[$_])"
                                        }
                                        $repeatable = [Google.Apis.Util.Repeatable[String]]::new([String[]]$converted)
                                        $request.PrivateExtendedProperty = $repeatable
                                    }
                                    SharedExtendedProperty {
                                        $converted = $SharedExtendedProperty.Keys | Foreach-Object {
                                            "$($_)=$($SharedExtendedProperty[$_])"
                                        }
                                        $repeatable = [Google.Apis.Util.Repeatable[String]]::new([String[]]$converted)
                                        $request.SharedExtendedProperty = $repeatable
                                    }
                                    Default {
                                        if ($request.PSObject.Properties.Name -contains $key) {
                                            $request.$key = $PSBoundParameters[$key]
                                        }
                                    }
                                }
                            }
                            if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                                $PageSize = $Limit
                            }
                            $request.MaxResults = $PageSize
                            if ($Filter) {
                                Write-Verbose "Getting all Calendar Events matching filter '$Filter' on calendar '$calId' for user '$U'"
                            }
                            else {
                                Write-Verbose "Getting all Calendar Events on calendar '$calId' for user '$U'"
                            }
                            [int]$i = 1
                            $overLimit = $false
                            do {
                                $result = $request.Execute()
                                $result.Items | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru | Add-Member -MemberType NoteProperty -Name 'CalendarId' -Value $calId -PassThru
                                if ($result.NextPageToken) {
                                    $request.PageToken = $result.NextPageToken
                                }
                                [int]$retrieved = ($i + $result.Items.Count) - 1
                                Write-Verbose "Retrieved $retrieved Calendar Events..."
                                if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                                    Write-Verbose "Limit reached: $Limit"
                                    $overLimit = $true
                                }
                                elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                                    $newPS = $Limit - $retrieved
                                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                                    $request.MaxResults = $newPS
                                }
                                [int]$i = $i + $result.Items.Count
                            }
                            until ($overLimit -or !$result.NextPageToken)
                        }
                        catch {
                            if ($ErrorActionPreference -eq 'Stop') {
                                $PSCmdlet.ThrowTerminatingError($_)
                            }
                            else {
                                Write-Error $_
                            }
                        }
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCalendarEvent'
function Get-GSCalendarSubscription {
    <#
    .SYNOPSIS
    Gets a subscribed calendar from a users calendar list. Returns the full calendar list if no CalendarId is specified.
 
    .DESCRIPTION
    Gets a subscribed calendar from a users calendar list. Returns the full calendar list if no CalendarId is specified.
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .PARAMETER CalendarID
    The calendar ID of the calendar you would like to get info for. If left blank, returns the list of calendars the user is subscribed to.
 
    .PARAMETER MinAccessRole
    The minimum access role for the user in the returned entries. Optional. The default is no restriction.
 
    .PARAMETER PageSize
    Maximum number of entries returned on one result page. The page size can never be larger than 250 entries.
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .PARAMETER ShowDeleted
    Whether to include deleted calendar list entries in the result. Optional. The default is False.
 
    .PARAMETER ShowHidden
    Whether to show hidden entries. Optional. The default is False.
 
    .PARAMETER SyncToken
    Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then.
 
    If only read-only fields such as calendar properties or ACLs have changed, the entry won't be returned. All entries deleted and hidden since the previous list request will always be in the result set and it is not allowed to set showDeleted neither showHidden to False.
 
    To ensure client state consistency minAccessRole query parameter cannot be specified together with nextSyncToken. If the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken. Learn more about incremental synchronization.
 
    Optional. The default is to return all entries.
 
    .EXAMPLE
    Get-GSCalendarSubscription
 
    Gets the AdminEmail user's calendar list
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/Get-GSCalendarSubscription/
 
    .LINK
    https://developers.google.com/calendar/v3/reference/calendarList/get
 
    .LINK
    https://developers.google.com/calendar/v3/reference/calendarList/list
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.CalendarListEntry')]
    [cmdletbinding(DefaultParameterSetName = 'List')]
    Param (
        [parameter(Mandatory,Position = 0,ValueFromPipelineByPropertyName,ParameterSetName = "Get")]
        [Alias('Id')]
        [String[]]
        $CalendarId,
        [parameter(ValueFromPipelineByPropertyName)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(ParameterSetName = "List")]
        [Google.Apis.Calendar.v3.CalendarListResource+ListRequest+MinAccessRoleEnum]
        $MinAccessRole,
        [parameter(ParameterSetName = "List")]
        [Alias('MaxResults')]
        [ValidateRange(1,250)]
        [Int]
        $PageSize = 250,
        [parameter(ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0,
        [parameter(ParameterSetName = "List")]
        [switch]
        $ShowDeleted,
        [parameter(ParameterSetName = "List")]
        [switch]
        $ShowHidden,
        [parameter(ParameterSetName = "List")]
        [String]
        $SyncToken
    )
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/calendar'
                ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                User        = $U
            }
            $service = New-GoogleService @serviceParams
            if ($PSCmdlet.ParameterSetName -eq 'Get') {
                foreach ($calId in $CalendarID) {
                    try {
                        Write-Verbose "Getting subscribed calendar '$($calId)' for user '$U'"
                        $request = $service.CalendarList.Get($calId)
                        $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            else {
                try {
                    $request = $service.CalendarList.List()
                    foreach ($key in $PSBoundParameters.Keys | Where-Object {$request.PSObject.Properties.Name -contains $_}) {
                        $request.$key = $PSBoundParameters[$key]
                    }
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.MaxResults = $PageSize
                    Write-Verbose "Getting CalendarList for user '$U'"
                    [int]$i = 1
                    $overLimit = $false
                    do {
                        $result = $request.Execute()
                        $result.Items | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                        if ($result.NextPageToken) {
                            $request.PageToken = $result.NextPageToken
                        }
                        [int]$retrieved = ($i + $result.Items.Count) - 1
                        Write-Verbose "Retrieved $retrieved Calendars..."
                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                            Write-Verbose "Limit reached: $Limit"
                            $overLimit = $true
                        }
                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                            $newPS = $Limit - $retrieved
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                            $request.MaxResults = $newPS
                        }
                        [int]$i = $i + $result.Items.Count
                    }
                    until ($overLimit -or !$result.NextPageToken)
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCalendarSubscription'
function New-GSCalendarAcl {
    <#
    .SYNOPSIS
    Adds a new Access Control Rule to a calendar.
 
    .DESCRIPTION
    Adds a new Access Control Rule to a calendar.
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config.
 
    .PARAMETER CalendarID
    The Id of the calendar you would like to share
 
    Defaults to the user's primary calendar.
 
    .PARAMETER Role
    The role assigned to the scope.
 
    Available values are:
    * "none" - Provides no access.
    * "freeBusyReader" - Provides read access to free/busy information.
    * "reader" - Provides read access to the calendar. Private events will appear to users with reader access, but event details will be hidden.
    * "writer" - Provides read and write access to the calendar. Private events will appear to users with writer access, and event details will be visible.
    * "owner" - Provides ownership of the calendar. This role has all of the permissions of the writer role with the additional ability to see and manipulate ACLs.
 
    .PARAMETER Value
    The email address of a user or group, or the name of a domain, depending on the scope type. Omitted for type "default".
 
    .PARAMETER Type
    The type of the scope.
 
    Available values are:
    * "default" - The public scope. This is the default value.
    * "user" - Limits the scope to a single user.
    * "group" - Limits the scope to a group.
    * "domain" - Limits the scope to a domain.
 
    Note: The permissions granted to the "default", or public, scope apply to any user, authenticated or not.
 
    .EXAMPLE
    New-GSCalendarACL -CalendarID jennyappleseed@domain.com -Role reader -Value Jonnyappleseed@domain.com -Type user
 
    Gives Jonnyappleseed@domain.com reader access to jennyappleseed's calendar.
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/New-GSCalendarACL/
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.AclRule')]
    [cmdletbinding(DefaultParameterSetName = "AttendeeEmails")]
    Param
    (
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = "me",
        [parameter(Mandatory = $false)]
        [String[]]
        $CalendarId = "primary",
        [parameter(Mandatory = $true)]
        [ValidateSet("owner", "writer", "reader", "none", "freeBusyReader")]
        [String]
        $Role,
        [parameter(Mandatory = $true)]
        [String]
        $Value,
        [parameter(Mandatory = $false)]
        [ValidateSet("default", "user", "group", "domain")]
        [String]
        $Type = "user"
    )
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/calendar'
                ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                User        = $U
            }
            $service = New-GoogleService @serviceParams
            foreach ($calId in $CalendarID) {
                try {
                    $body = New-Object 'Google.Apis.Calendar.v3.Data.AclRule'
                    $scopeData = New-Object "Google.Apis.Calendar.v3.Data.AclRule+ScopeData"
                    foreach ($key in $PSBoundParameters.Keys) {
                        switch ($key) {
                            Role {
                                $body.Role = $PSBoundParameters[$key]
                            }
                            Type {
                                $scopeData.Type = $PSBoundParameters[$key]
                            }
                            Value {
                                $scopeData.Value = $PSBoundParameters[$key]
                            }
                            Default {
                                if ($body.PSObject.Properties.Name -contains $key) {
                                    $body.$key = $PSBoundParameters[$key]
                                }
                            }
                        }
                    }
                    Write-Verbose "Inserting new ACL for type '$Type' with value '$Value' in role '$Role' on calendar '$calId' for user '$U'"
                    $body.Scope = $scopeData
                    $request = $service.Acl.Insert($body, $calId)
                    $request.Execute()
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSCalendarACL'
function New-GSCalendarEvent {
    <#
    .SYNOPSIS
    Creates a new calendar event
 
    .DESCRIPTION
    Creates a new calendar event
 
    .PARAMETER Summary
    Event summary
 
    .PARAMETER Description
    Event description
 
    .PARAMETER Id
    Opaque identifier of the event. When creating new single or recurring events, you can specify their IDs. Provided IDs must follow these rules:
 
    * characters allowed in the ID are those used in base32hex encoding, i.e. lowercase letters a-v and digits 0-9, see section 3.1.2 in RFC2938
    * the length of the ID must be between 5 and 1024 characters
    * the ID must be unique per calendar
 
    Due to the globally distributed nature of the system, we cannot guarantee that ID collisions will be detected at event creation time. To minimize the risk of collisions we recommend using an established UUID algorithm such as one described in RFC4122.
 
    If you do not specify an ID, it will be automatically generated by the server.
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config.
 
    .PARAMETER CalendarID
    The calendar ID of the calendar you would like to list events from.
 
    Defaults to the user's primary calendar.
 
    .PARAMETER AttendeeEmails
    The email addresses of the attendees to add.
 
    NOTE: This performs simple adds without additional attendee options. If additional options are needed, use the Attendees parameter instead.
 
    .PARAMETER Attendees
    The EventAttendee object(s) to add. Use Add-GSEventAttendee with this parameter for best results.
 
    .PARAMETER Location
    Event location
 
    .PARAMETER Visibility
    Visibility of the event.
 
    Possible values are:
    * "default" - Uses the default visibility for events on the calendar. This is the default value.
    * "public" - The event is public and event details are visible to all readers of the calendar.
    * "private" - The event is private and only event attendees may view event details.
    * "confidential" - The event is private. This value is provided for compatibility reasons.
 
    .PARAMETER EventColor
    Color of the event as seen in Calendar
 
    .PARAMETER Reminders
    A list of reminders to add to this calendar event other than the default calendar reminder.
 
    This parameter expects a 'Google.Apis.Calendar.v3.Data.EventReminder[]' object type. You can create objects of this type easily by using the function 'Add-GSCalendarEventReminder'
 
    .PARAMETER DisableDefaultReminder
    When $true, disables inheritance of the default Reminders from the Calendar the event was created on.
 
    .PARAMETER LocalStartDateTime
    Start date and time of the event. Lowest precendence of the three StartDate parameters.
 
    Defaults to the time the function is ran.
 
    .PARAMETER LocalEndDateTime
    End date and time of the event. Lowest precendence of the three EndDate parameters.
 
    Defaults to 30 minutes after the time the function is ran.
 
    .PARAMETER StartDate
    String representation of the start date. Middle precendence of the three StartDate parameters.
 
    .PARAMETER EndDate
    String representation of the end date. Middle precendence of the three EndDate parameters.
 
    .PARAMETER UTCStartDateTime
    String representation of the start date in UTC. Highest precendence of the three StartDate parameters.
 
    .PARAMETER UTCEndDateTime
    String representation of the end date in UTC. Highest precendence of the three EndDate parameters.
 
    .PARAMETER PrivateExtendedProperties
    A hashtable of properties that are private to the copy of the event that appears on this calendar.
 
    .PARAMETER SharedExtendedProperties
    A hashtable of properties that are shared between copies of the event on other attendees' calendars.
 
    .PARAMETER ExtendedProperties
    Extended properties of the event. This must be of the type 'Google.Apis.Calendar.v3.Data.Event+ExtendedPropertiesData'.
 
    This is useful for copying another events ExtendedProperties over when creating a new event.
 
    .PARAMETER CreateMeetEvent
    Create a Google Meet conference event while creating the calendar event.
 
    This is useful for creating a Google Meet URL which you can send to people for video conferences.
 
    .EXAMPLE
    New-GSCalendarEvent "Go to the gym" -StartDate (Get-Date "21:00:00") -EndDate (Get-Date "22:00:00")
 
    Creates an event titled "Go to the gym" for 9-10PM the day the function is ran.
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/New-GSCalendarEvent/
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.Event')]
    [cmdletbinding(DefaultParameterSetName = "AttendeeEmails")]
    Param
    (
        [parameter(Mandatory,Position = 0)]
        [String]
        $Summary,
        [parameter()]
        [String]
        $Description,
        [parameter()]
        [ValidateScript( { if ($_ -match '^[0-9a-v]+$') {
                    $true
                }
                else {
                    throw "The characters allowed in the ID are only those used in base32hex encoding, i.e. lowercase letters a-v and digits 0-9"
                } })]
        [ValidateLength(5,1024)]
        [String]
        $Id,
        [parameter(ValueFromPipelineByPropertyName)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(ValueFromPipelineByPropertyName)]
        [String[]]
        $CalendarID = "primary",
        [parameter(ParameterSetName = "AttendeeEmails")]
        [String[]]
        $AttendeeEmails,
        [parameter(ParameterSetName = "AttendeeObjects")]
        [Google.Apis.Calendar.v3.Data.EventAttendee[]]
        $Attendees,
        [parameter()]
        [String]
        $Location,
        [parameter()]
        [ValidateSet('default','public','private','confidential')]
        [String]
        $Visibility,
        [parameter()]
        [ValidateSet("Periwinkle","Seafoam","Lavender","Coral","Goldenrod","Beige","Cyan","Grey","Blue","Green","Red")]
        [String]
        $EventColor,
        [parameter()]
        [Google.Apis.Calendar.v3.Data.EventReminder[]]
        $Reminders,
        [parameter()]
        [Alias('DisableReminder')]
        [Switch]
        $DisableDefaultReminder,
        [parameter()]
        [DateTime]
        $LocalStartDateTime = (Get-Date),
        [parameter()]
        [DateTime]
        $LocalEndDateTime = (Get-Date).AddMinutes(30),
        [parameter()]
        [String]
        $StartDate,
        [parameter()]
        [String]
        $EndDate,
        [parameter()]
        [String]
        $UTCStartDateTime,
        [parameter()]
        [String]
        $UTCEndDateTime,
        [parameter()]
        [Hashtable]
        $PrivateExtendedProperties,
        [parameter()]
        [Hashtable]
        $SharedExtendedProperties,
        [parameter()]
        [Google.Apis.Calendar.v3.Data.Event+ExtendedPropertiesData]
        $ExtendedProperties,
        [parameter()]
        [switch]
        $CreateMeetEvent
    )
    Begin {
        $colorHash = @{
            Periwinkle = 1
            Seafoam    = 2
            Lavender   = 3
            Coral      = 4
            Goldenrod  = 5
            Beige      = 6
            Cyan       = 7
            Grey       = 8
            Blue       = 9
            Green      = 10
            Red        = 11
        }
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                $serviceParams = @{
                    Scope       = 'https://www.googleapis.com/auth/calendar'
                    ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                    User        = $U
                }
                $service = New-GoogleService @serviceParams
                if ($PSCmdlet.ParameterSetName -eq 'AttendeeEmails' -and $PSBoundParameters.Keys -contains 'AttendeeEmails') {
                    [Google.Apis.Calendar.v3.Data.EventAttendee[]]$Attendees = $AttendeeEmails | ForEach-Object {
                        Add-GSEventAttendee -Email $_
                    }
                }
                $body = New-Object 'Google.Apis.Calendar.v3.Data.Event'
                if ($Attendees) {
                    $body.Attendees = [Google.Apis.Calendar.v3.Data.EventAttendee[]]$Attendees
                }
                $RemindersData = $null
                foreach ($key in $PSBoundParameters.Keys) {
                    switch ($key) {
                        EventColor {
                            $body.ColorId = $colorHash[$EventColor]
                        }
                        PrivateExtendedProperties {
                            if (-not $ExtendedProperties) {
                                $ExtendedProperties = New-Object 'Google.Apis.Calendar.v3.Data.Event+ExtendedPropertiesData' -Property @{
                                    Private__ = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                                    Shared    = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                                }
                            }
                            elseif (-not $ExtendedProperties.Private__) {
                                $ExtendedProperties.Private__ = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                            }
                            foreach ($prop in $PrivateExtendedProperties.Keys) {
                                $ExtendedProperties.Private__.Add($prop,$PrivateExtendedProperties[$prop])
                            }
                        }
                        SharedExtendedProperties {
                            if (-not $ExtendedProperties) {
                                $ExtendedProperties = New-Object 'Google.Apis.Calendar.v3.Data.Event+ExtendedPropertiesData' -Property @{
                                    Private__ = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                                    Shared    = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                                }
                            }
                            elseif (-not $ExtendedProperties.Shared) {
                                $ExtendedProperties.Shared = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                            }
                            foreach ($prop in $SharedExtendedProperties.Keys) {
                                $ExtendedProperties.Shared.Add($prop,$SharedExtendedProperties[$prop])
                            }
                        }
                        Reminders {
                            if ($null -eq $RemindersData) {
                                $RemindersData = New-Object 'Google.Apis.Calendar.v3.Data.Event+RemindersData'
                            }
                            $RemindersData.Overrides = $Reminders
                        }
                        DisableDefaultReminder {
                            if ($null -eq $RemindersData) {
                                $RemindersData = New-Object 'Google.Apis.Calendar.v3.Data.Event+RemindersData'
                            }
                            $RemindersData.UseDefault = (-not $DisableDefaultReminder)
                        }
                        Default {
                            if ($body.PSObject.Properties.Name -contains $key) {
                                $body.$key = $PSBoundParameters[$key]
                            }
                        }
                    }
                }
                if ($RemindersData) {
                    $body.Reminders = $RemindersData
                }
                if ($ExtendedProperties) {
                    $body.ExtendedProperties = $ExtendedProperties
                }
                $body.Start = if ($UTCStartDateTime) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        DateTime = $UTCStartDateTime
                    }
                }
                elseif ($StartDate) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        Date = (Get-Date $StartDate -Format "yyyy-MM-dd")
                    }
                }
                else {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        DateTime = $LocalStartDateTime
                    }
                }
                $body.End = if ($UTCEndDateTime) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        DateTime = $UTCEndDateTime
                    }
                }
                elseif ($EndDate) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        Date = (Get-Date $EndDate -Format "yyyy-MM-dd")
                    }
                }
                else {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        DateTime = $LocalEndDateTime
                    }
                }
                $verbMsg = $null
                if ($CreateMeetEvent) {
                    $createRequest = New-Object 'Google.Apis.Calendar.v3.Data.CreateConferenceRequest'
                    $createRequest.RequestId = (New-Guid).ToString('n')
                    $confData = New-Object 'Google.Apis.Calendar.v3.Data.ConferenceData'
                    $confData.CreateRequest = $createRequest
                    $body.ConferenceData = $confData
                    $verbMsg = ' with Meet conferencing'
                }
                foreach ($calId in $CalendarID) {
                    Write-Verbose "Creating Calendar Event '$($Summary)'$($verbMsg) on calendar '$calId' for user '$U'"
                    $request = $service.Events.Insert($body,$calId)
                    if ($CreateMeetEvent) {
                        $request.ConferenceDataVersion = 1
                    }
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru | Add-Member -MemberType NoteProperty -Name 'CalendarId' -Value $calId -PassThru
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSCalendarEvent'
function Remove-GSCalendarAcl {
    <#
    .SYNOPSIS
    Removes an Access Control List rule from a calendar.
 
    .DESCRIPTION
    Removes an Access Control List rule from a calendar.
 
    .PARAMETER User
    The primary email or UserID of the user who owns the calendar. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    .PARAMETER CalendarID
    The calendar ID of the calendar you would like to remove the ACL from.
 
    .PARAMETER RuleId
    The ACL rule Id to remove.
 
    .EXAMPLE
    Get-GSCalendar -User joe@domain.com |
        Get-GSCalendarAcl |
        Where-Object {$_.Role -eq 'Owner'} |
        Remove-GSCalendarAcl
 
    Gets all the calendars for Joe and finds all ACL rules where
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/Remove-GSCalendarAcl/
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [String]
        $CalendarID,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $RuleId
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/calendar'
            ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($rule in $RuleId) {
            try {
                if ($PSCmdlet.ShouldProcess("Deleting ACL Rule Id '$rule' from Calendar '$CalendarID' for user '$User'")) {
                    Write-Verbose "Deleting ACL Rule Id '$rule' from Calendar '$CalendarID' for user '$User'"
                    $request = $service.Acl.Delete($CalendarID, $rule)
                    $request.Execute()
                    Write-Verbose "ACL Rule Id '$rule' deleted successfully from Calendar '$CalendarID' for user '$User'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSCalendarAcl'
function Remove-GSCalendarEvent {
    <#
    .SYNOPSIS
    Removes a calendar event
 
    .DESCRIPTION
    Removes a calendar event
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config.
 
    .PARAMETER CalendarID
    The calendar ID of the calendar you would like to remove events from.
 
    Defaults to the user's primary calendar.
 
    .PARAMETER EventID
    The EventID to remove
 
    .EXAMPLE
    Remove-GSCalendarEvent -User user@domain.com -EventID _60q30c1g60o30e1i60o4ac1g60rj8gpl88rj2c1h84s34h9g60s30c1g60o30c1g84o3eg9n8gq32d246gq48d1g64o30c1g60o30c1g60o30c1g60o32c1g60o30c1g8csjihhi6oq3igi28h248ghk6ks4agq161144ga46gr4aci488p0
 
    Removes the specified event from user@domain.com's calendar.
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/Remove-GSCalendarEvent/
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $CalendarID = "primary",
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $EventID
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/calendar'
            ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($E in $EventId) {
            try {
                if ($PSCmdlet.ShouldProcess("Deleting Event Id '$E' from user '$User'")) {
                    Write-Verbose "Deleting Event Id '$E' from user '$User'"
                    $request = $service.Events.Delete($CalendarID, $E)
                    $request.Execute()
                    Write-Verbose "Label Id '$E' deleted successfully from user '$User'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSCalendarEvent'
function Remove-GSCalendarSubscription {
    <#
    .SYNOPSIS
    Removes a calendar from a users calendar list (aka unsubscribes from the specified calendar)
 
    .DESCRIPTION
    Removes a calendar from a users calendar list (aka unsubscribes from the specified calendar)
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    .PARAMETER CalendarID
    The calendar ID of the calendar you would like to unsubscribe the user from
 
    .EXAMPLE
    Remove-GSCalendarSubscription -User me -CalendarId john.smith@domain.com
 
    Removes the calendar 'john.smith@domain.com' from the AdminEmail user's calendar list
 
    .LINK
    https://psgsuite.io/Function%20Help/Calendar/Remove-GSCalendarSubscription/
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory = $true,Position = 1)]
        [String[]]
        $CalendarId
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/calendar'
            ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($calId in $CalendarID) {
            try {
                if ($PSCmdlet.ShouldProcess("Unsubscribing user '$User' from Calendar '$($calId)'")) {
                    Write-Verbose "Unsubscribing user '$User' from Calendar '$($calId)'"
                    $request = $service.CalendarList.Delete($calId)
                    $request.Execute()
                    Write-Verbose "User '$User' has been successfully unsubscribed from calendar '$calId'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSCalendarSubscription'
function Update-GSCalendarEvent {
    <#
    .SYNOPSIS
    Updates an event
 
    .DESCRIPTION
    Updates an event
 
    .PARAMETER EventID
    The unique Id of the event to update
 
    .PARAMETER CalendarID
    The Id of the calendar
 
    Defaults to the user's primary calendar.
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config.
 
    .PARAMETER Summary
    Event summary
 
    .PARAMETER Description
    Event description
 
    .PARAMETER AttendeeEmails
    The email addresses of the attendees to add.
 
    NOTE: This performs simple adds without additional attendee options. If additional options are needed, use the Attendees parameter instead.
 
    .PARAMETER Attendees
    The EventAttendee object(s) to add. Use Add-GSEventAttendee with this parameter for best results.
 
    .PARAMETER Location
    Event location
 
    .PARAMETER Visibility
    Visibility of the event.
 
    Possible values are:
    * "default" - Uses the default visibility for events on the calendar. This is the default value.
    * "public" - The event is public and event details are visible to all readers of the calendar.
    * "private" - The event is private and only event attendees may view event details.
    * "confidential" - The event is private. This value is provided for compatibility reasons.
 
    .PARAMETER EventColor
    Color of the event as seen in Calendar
 
    .PARAMETER Reminders
    A list of reminders to add to this calendar event.
 
    This parameter expects a 'Google.Apis.Calendar.v3.Data.EventReminder[]' object type. You can create objects of this type easily by using the function 'Add-GSCalendarEventReminder'
 
    .PARAMETER RemoveAllReminders
    If $true, removes all reminder overrides and disables the default reminder inheritance from the calendar that the event is on.
 
    .PARAMETER DisableDefaultReminder
    When $true, disables inheritance of the default Reminders from the Calendar the event was created on.
 
    .PARAMETER LocalStartDateTime
    Start date and time of the event. Lowest precendence of the three StartDate parameters.
 
    Defaults to the time the function is ran.
 
    .PARAMETER LocalEndDateTime
    End date and time of the event. Lowest precendence of the three EndDate parameters.
 
    Defaults to 30 minutes after the time the function is ran.
 
    .PARAMETER StartDate
    String representation of the start date. Middle precendence of the three StartDate parameters.
 
    .PARAMETER EndDate
    String representation of the end date. Middle precendence of the three EndDate parameters.
 
    .PARAMETER UTCStartDateTime
    String representation of the start date in UTC. Highest precendence of the three StartDate parameters.
 
    .PARAMETER UTCEndDateTime
    String representation of the end date in UTC. Highest precendence of the three EndDate parameters.
 
    .PARAMETER PrivateExtendedProperties
    A hashtable of properties that are private to the copy of the event that appears on this calendar.
 
    .PARAMETER SharedExtendedProperties
    A hashtable of properties that are shared between copies of the event on other attendees' calendars.
 
    .PARAMETER ExtendedProperties
    Extended properties of the event. This must be of the type 'Google.Apis.Calendar.v3.Data.Event+ExtendedPropertiesData'.
 
    This is useful for copying another events ExtendedProperties over when updating an existing event.
 
    .EXAMPLE
    New-GSCalendarEvent "Go to the gym" -StartDate (Get-Date "21:00:00") -EndDate (Get-Date "22:00:00")
 
    Creates an event titled "Go to the gym" for 9-10PM the day the function is ran.
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.Event')]
    [cmdletbinding(DefaultParameterSetName = "AttendeeEmails")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $EventId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String[]]
        $CalendarId = "primary",
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [String]
        $Summary,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false,ParameterSetName = "AttendeeEmails")]
        [String[]]
        $AttendeeEmails,
        [parameter(Mandatory = $false,ParameterSetName = "AttendeeObjects")]
        [Google.Apis.Calendar.v3.Data.EventAttendee[]]
        $Attendees,
        [parameter(Mandatory = $false)]
        [String]
        $Location,
        [parameter(Mandatory = $false)]
        [ValidateSet('default','public','private','confidential')]
        [String]
        $Visibility,
        [parameter(Mandatory = $false)]
        [ValidateSet("Periwinkle","Seafoam","Lavender","Coral","Goldenrod","Beige","Cyan","Grey","Blue","Green","Red")]
        [String]
        $EventColor,
        [parameter()]
        [Google.Apis.Calendar.v3.Data.EventReminder[]]
        $Reminders,
        [parameter(Mandatory = $false)]
        [Switch]
        $RemoveAllReminders,
        [parameter(Mandatory = $false)]
        [Alias('DisableReminder')]
        [Switch]
        $DisableDefaultReminder,
        [parameter(Mandatory = $false)]
        [DateTime]
        $LocalStartDateTime,
        [parameter(Mandatory = $false)]
        [DateTime]
        $LocalEndDateTime,
        [parameter(Mandatory = $false)]
        [String]
        $StartDate,
        [parameter(Mandatory = $false)]
        [String]
        $EndDate,
        [parameter(Mandatory = $false)]
        [String]
        $UTCStartDateTime,
        [parameter(Mandatory = $false)]
        [String]
        $UTCEndDateTime,
        [parameter(Mandatory = $false)]
        [Hashtable]
        $PrivateExtendedProperties,
        [parameter(Mandatory = $false)]
        [Hashtable]
        $SharedExtendedProperties,
        [parameter(Mandatory = $false)]
        [Google.Apis.Calendar.v3.Data.Event+ExtendedPropertiesData]
        $ExtendedProperties
    )
    Begin {
        $colorHash = @{
            Periwinkle = 1
            Seafoam    = 2
            Lavender   = 3
            Coral      = 4
            Goldenrod  = 5
            Beige      = 6
            Cyan       = 7
            Grey       = 8
            Blue       = 9
            Green      = 10
            Red        = 11
        }
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                $serviceParams = @{
                    Scope       = 'https://www.googleapis.com/auth/calendar'
                    ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                    User        = $U
                }
                $service = New-GoogleService @serviceParams
                if ($PSCmdlet.ParameterSetName -eq 'AttendeeEmails' -and $PSBoundParameters.Keys -contains 'AttendeeEmails') {
                    [Google.Apis.Calendar.v3.Data.EventAttendee[]]$Attendees = $AttendeeEmails | ForEach-Object {
                        Add-GSEventAttendee -Email $_
                    }
                }
                $body = New-Object 'Google.Apis.Calendar.v3.Data.Event'
                if ($Attendees) {
                    $body.Attendees = [Google.Apis.Calendar.v3.Data.EventAttendee[]]$Attendees
                }
                $RemindersData = $null
                foreach ($key in $PSBoundParameters.Keys) {
                    switch ($key) {
                        EventColor {
                            $body.ColorId = $colorHash[$EventColor]
                        }
                        PrivateExtendedProperties {
                            if (-not $ExtendedProperties) {
                                $ExtendedProperties = New-Object 'Google.Apis.Calendar.v3.Data.Event+ExtendedPropertiesData' -Property @{
                                    Private__ = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                                    Shared = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                                }
                            }
                            elseif (-not $ExtendedProperties.Private__) {
                                $ExtendedProperties.Private__ = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                            }
                            foreach ($prop in $PrivateExtendedProperties.Keys) {
                                $ExtendedProperties.Private__.Add($prop,$PrivateExtendedProperties[$prop])
                            }
                        }
                        SharedExtendedProperties {
                            if (-not $ExtendedProperties) {
                                $ExtendedProperties = New-Object 'Google.Apis.Calendar.v3.Data.Event+ExtendedPropertiesData' -Property @{
                                    Private__ = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                                    Shared = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                                }
                            }
                            elseif (-not $ExtendedProperties.Shared) {
                                $ExtendedProperties.Shared = (New-Object 'System.Collections.Generic.Dictionary[string,string]')
                            }
                            foreach ($prop in $SharedExtendedProperties.Keys) {
                                $ExtendedProperties.Shared.Add($prop,$SharedExtendedProperties[$prop])
                            }
                        }
                        Reminders {
                            if ($null -eq $RemindersData) {
                                $RemindersData = New-Object 'Google.Apis.Calendar.v3.Data.Event+RemindersData'
                            }
                            $RemindersData.Overrides = $Reminders
                        }
                        RemoveAllReminders {
                            $RemindersData = New-Object 'Google.Apis.Calendar.v3.Data.Event+RemindersData' -Property @{
                                UseDefault = $false
                                Overrides = New-Object 'System.Collections.Generic.List[Google.Apis.Calendar.v3.Data.EventReminder]'
                            }
                        }
                        DisableDefaultReminder {
                            if ($null -eq $RemindersData) {
                                $RemindersData = New-Object 'Google.Apis.Calendar.v3.Data.Event+RemindersData'
                            }
                            $RemindersData.UseDefault = (-not $DisableDefaultReminder)
                        }
                        Default {
                            if ($body.PSObject.Properties.Name -contains $key) {
                                $body.$key = $PSBoundParameters[$key]
                            }
                        }
                    }
                }
                if ($RemindersData) {
                    $body.Reminders = $RemindersData
                }
                if ($ExtendedProperties) {
                    $body.ExtendedProperties = $ExtendedProperties
                }
                $body.Start = if ($UTCStartDateTime) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        DateTime = $UTCStartDateTime
                    }
                }
                elseif ($StartDate) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        Date = (Get-Date $StartDate -Format "yyyy-MM-dd")
                    }
                }
                elseif ($LocalStartDateTime) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        DateTime = $LocalStartDateTime
                    }
                }
                $body.End = if ($UTCEndDateTime) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        DateTime = $UTCEndDateTime
                    }
                }
                elseif ($EndDate) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        Date = (Get-Date $EndDate -Format "yyyy-MM-dd")
                    }
                }
                elseif ($LocalEndDateTime) {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventDateTime' -Property @{
                        DateTime = $LocalEndDateTime
                    }
                }
                foreach ($calId in $CalendarID) {
                    foreach ($evId in $EventId) {
                        Write-Verbose "Updating Calendar Event '$evId' on calendar '$calId' for user '$U'"
                        $request = $service.Events.Patch($body,$calId,$evId)
                        $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru | Add-Member -MemberType NoteProperty -Name 'CalendarId' -Value $calId -PassThru | Add-Member -MemberType NoteProperty -Name 'EventId' -Value $evId -PassThru
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSCalendarEvent'
function Update-GSCalendarSubscription {
    <#
    .SYNOPSIS
    Updates a calendar in a users calendar list (aka subscription)
 
    .DESCRIPTION
    Updates a calendar in a users calendar list (aka subscription)
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    .PARAMETER CalendarID
    The calendar ID of the calendar subscription you would like to update.
 
    .PARAMETER ColorRgbFormat
    Whether to use the foregroundColor and backgroundColor fields to write the calendar colors (RGB). If this feature is used, the index-based colorId field will be set to the best matching option automatically.
 
    .PARAMETER Selected
    Whether the calendar content shows up in the calendar UI. Optional. The default is False.
 
    .PARAMETER Hidden
    Whether the calendar has been hidden from the list. Optional. The default is False.
 
    .PARAMETER Reminders
    A list of reminders to add to this calendar.
 
    This parameter expects a 'Google.Apis.Calendar.v3.Data.EventReminder[]' object type. You can create objects of this type easily by using the function 'Add-GSCalendarEventReminder'
 
    .PARAMETER RemoveReminders
    If $true, removes reminders from this CalendarSubscription.
 
    .PARAMETER DefaultReminderMethod
    The method used by this reminder. Defaults to email.
 
    Possible values are:
    * "email" - Reminders are sent via email.
    * "sms" - Reminders are sent via SMS. These are only available for G Suite customers. Requests to set SMS reminders for other account types are ignored.
    * "popup" - Reminders are sent via a UI popup.
 
    .PARAMETER DefaultReminderMinutes
    Number of minutes before the start of the event when the reminder should trigger. Defaults to 30 minutes.
 
    Valid values are between 0 and 40320 (4 weeks in minutes).
 
    .PARAMETER Notifications
    A list of notifications to add to this calendar.
 
    This parameter expects a 'Google.Apis.Calendar.v3.Data.CalendarNotification[]' object type. You can create objects of this type easily by using the function 'Add-GSCalendarNotification'
 
    .PARAMETER RemoveNotifications
    If $true, removes notifications from this CalendarSubscription.
 
    .PARAMETER DefaultNotificationMethod
    The method used to deliver the notification.
 
    Possible values are:
    * "email" - Reminders are sent via email.
    * "sms" - Reminders are sent via SMS. This value is read-only and is ignored on inserts and updates. SMS reminders are only available for G Suite customers.
 
    .PARAMETER DefaultNotificationType
    The type of notification.
 
    Possible values are:
    * "eventCreation" - Notification sent when a new event is put on the calendar.
    * "eventChange" - Notification sent when an event is changed.
    * "eventCancellation" - Notification sent when an event is cancelled.
    * "eventResponse" - Notification sent when an event is changed.
    * "agenda" - An agenda with the events of the day (sent out in the morning).
 
    .PARAMETER Color
    The color of the calendar.
 
    .PARAMETER SummaryOverride
    The summary that the authenticated user has set for this calendar.
 
    .EXAMPLE
    Update-GSCalendarSubscription -User me -CalendarId john.smith@domain.com -Selected -Color Cyan
 
    Updates the calendar 'john.smith@domain.com' on the AdminEmail user's calendar list by marking it as selected and setting the color to Cyan
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.CalendarListEntry')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory,Position = 1,ValueFromPipelineByPropertyName)]
        [Alias('Id')]
        [String[]]
        $CalendarId,
        [parameter(Mandatory = $false)]
        [Switch]
        $ColorRgbFormat,
        [parameter(Mandatory = $false)]
        [Switch]
        $Selected,
        [parameter(Mandatory = $false)]
        [Switch]
        $Hidden,
        [parameter()]
        [Google.Apis.Calendar.v3.Data.EventReminder[]]
        $Reminders,
        [parameter()]
        [switch]
        $RemoveReminders,
        [parameter(Mandatory = $false)]
        [ValidateSet('email','sms','popup')]
        [String]
        $DefaultReminderMethod,
        [parameter(Mandatory = $false)]
        [ValidateRange(0,40320)]
        [Int]
        $DefaultReminderMinutes = 10,
        [parameter()]
        [Google.Apis.Calendar.v3.Data.CalendarNotification[]]
        $Notifications,
        [parameter()]
        [switch]
        $RemoveNotifications,
        [parameter(Mandatory = $false)]
        [ValidateSet('email','sms')]
        [String]
        $DefaultNotificationMethod,
        [parameter(Mandatory = $false)]
        [ValidateSet('eventCreation','eventChange','eventCancellation','eventResponse','agenda')]
        [String]
        $DefaultNotificationType,
        [parameter(Mandatory = $false)]
        [ValidateSet("Periwinkle","Seafoam","Lavender","Coral","Goldenrod","Beige","Cyan","Grey","Blue","Green","Red")]
        [String]
        $Color,
        [parameter(Mandatory = $false)]
        [String]
        $SummaryOverride
    )
    Begin {
        $colorHash = @{
            Periwinkle = 1
            Seafoam    = 2
            Lavender   = 3
            Coral      = 4
            Goldenrod  = 5
            Beige      = 6
            Cyan       = 7
            Grey       = 8
            Blue       = 9
            Green      = 10
            Red        = 11
        }
    }
    Process {
        try {
            if ($User -ceq 'me') {
                $User = $Script:PSGSuite.AdminEmail
            }
            elseif ($User -notlike "*@*.*") {
                $User = "$($User)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/calendar'
                ServiceType = 'Google.Apis.Calendar.v3.CalendarService'
                User        = $User
            }
            $service = New-GoogleService @serviceParams
            foreach ($calId in $CalendarID) {
                $body = Get-GSCalendarSubscription -User $User -CalendarId $calId -Verbose:$false
                if ($RemoveReminders) {
                    $body.DefaultReminders = New-Object 'System.Collections.Generic.List[Google.Apis.Calendar.v3.Data.EventReminder]'
                }
                elseif ($PSBoundParameters.ContainsKey('Reminders')) {
                    if ($null -eq $body.DefaultReminders) {
                        $body.DefaultReminders = $Reminders
                    }
                    else {
                        $Reminders | ForEach-Object {
                            $body.DefaultReminders.Add($_) | Out-Null
                        }
                    }
                }
                elseif ($PSBoundParameters.ContainsKey('DefaultReminderMethod')) {
                    $defReminder = New-Object 'Google.Apis.Calendar.v3.Data.EventReminder' -Property @{
                        Method = $DefaultReminderMethod
                        Minutes = $DefaultReminderMinutes
                    }
                    if ($null -eq $body.DefaultReminders) {
                        $body.DefaultReminders = [Google.Apis.Calendar.v3.Data.EventReminder[]]$defReminder
                    }
                    else {
                        $body.DefaultReminders.Add($defReminder)
                    }
                }
                if ($RemoveNotifications) {
                    $body.NotificationSettings = New-Object 'Google.Apis.Calendar.v3.Data.CalendarListEntry+NotificationSettingsData' -Property @{
                        Notifications = New-Object 'System.Collections.Generic.List[Google.Apis.Calendar.v3.Data.CalendarNotification]'
                    }
                }
                elseif ($PSBoundParameters.ContainsKey('Notifications')) {
                    if ($null -eq $body.NotificationSettings) {
                        $body.NotificationSettings = New-Object 'Google.Apis.Calendar.v3.Data.CalendarListEntry+NotificationSettingsData' -Property @{
                            Notifications = [Google.Apis.Calendar.v3.Data.CalendarNotification[]]$Notifications
                        }
                    }
                    else {
                        $Notifications | ForEach-Object {
                            $body.NotificationSettings.Notifications.Add($_) | Out-Null
                        }
                    }
                }
                elseif ($PSBoundParameters.ContainsKey('DefaultNotificationMethod') -and $PSBoundParameters.ContainsKey('DefaultNotificationType')) {
                    if ($null -eq $body.NotificationSettings) {
                        $DefaultNotification = New-Object 'Google.Apis.Calendar.v3.Data.CalendarNotification' -Property @{
                            Method = $DefaultNotificationMethod
                            Type = $DefaultNotificationType
                        }
                        $body.NotificationSettings = New-Object 'Google.Apis.Calendar.v3.Data.CalendarListEntry+NotificationSettingsData' -Property @{
                            Notifications = [Google.Apis.Calendar.v3.Data.CalendarNotification[]]$DefaultNotification
                        }
                    }
                    else {
                        $DefaultNotification = New-Object 'Google.Apis.Calendar.v3.Data.CalendarNotification' -Property @{
                            Method = $DefaultNotificationMethod
                            Type = $DefaultNotificationType
                        }
                        $body.NotificationSettings.Notifications.Add($DefaultNotification) | Out-Null
                    }
                }
                foreach ($key in $PSBoundParameters.Keys) {
                    switch ($key) {
                        Color {
                            $body.ColorId = $colorHash[$Color]
                        }
                        Default {
                            if ($body.PSObject.Properties.Name -contains $key) {
                                $body.$key = $PSBoundParameters[$key]
                            }
                        }
                    }
                }
                Write-Verbose "Updating Calendar '$($calId) for user '$User'"
                $request = $service.CalendarList.Patch($body,$calId)
                if ($PSBoundParameters.ContainsKey('ColorRgbFormat')) {
                    $request.ColorRgbFormat = $ColorRgbFormat
                }
                $request.Execute()
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSCalendarSubscription'
function Get-GSChatMember {
    <#
    .SYNOPSIS
    Gets Chat member information
 
    .DESCRIPTION
    Gets Chat member information
 
    .PARAMETER Member
    Resource name of the membership to be retrieved, in the form "spaces/members".
 
    Example: spaces/AAAAMpdlehY/members/105115627578887013105
 
    .PARAMETER Space
    The resource name of the space for which membership list is to be fetched, in the form "spaces".
 
    Example: spaces/AAAAMpdlehY
 
    .EXAMPLE
    Get-GSChatMember -Space 'spaces/AAAAMpdlehY'
 
    Gets the list of human members in the Chat space specified
    #>

    [OutputType('Google.Apis.HangoutsChat.v1.Data.Membership')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,ParameterSetName = "Get")]
        [string[]]
        $Member,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "List")]
        [Alias('Parent','Name')]
        [string[]]
        $Space
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/chat.bot'
            ServiceType = 'Google.Apis.HangoutsChat.v1.HangoutsChatService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($mem in $Member) {
                    try {
                        $request = $service.Spaces.Members.Get($mem)
                        Write-Verbose "Getting Member '$mem'"
                        $request.Execute()
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            List {
                foreach ($sp in $Space) {
                    try {
                        if ($sp -notlike "spaces/*") {
                            try {
                                $sp = Get-GSChatConfig -SpaceName $sp -ErrorAction Stop
                            }
                            catch {
                                $sp = "spaces/$sp"
                            }
                        }
                        $request = $service.Spaces.Members.List($sp)
                        Write-Verbose "Getting Member List of Chat Space '$sp'"
                        [int]$i = 1
                        do {
                            $result = $request.Execute()
                            $result.Memberships
                            if ($result.NextPageToken) {
                                $request.PageToken = $result.NextPageToken
                            }
                            [int]$retrieved = ($i + $result.Memberships.Count) - 1
                            Write-Verbose "Retrieved $retrieved Memberships..."
                            [int]$i = $i + $result.Memberships.Count
                        }
                        until (!$result.NextPageToken)
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSChatMember'
function Get-GSChatMessage {
    <#
    .SYNOPSIS
    Gets a Chat message
 
    .DESCRIPTION
    Gets a Chat message
 
    .PARAMETER Name
    Resource name of the message to be retrieved, in the form "spaces/messages".
 
    Example: spaces/AAAAMpdlehY/messages/UMxbHmzDlr4.UMxbHmzDlr4
 
    .EXAMPLE
    Get-GSChatMessage -Name 'spaces/AAAAMpdlehY/messages/UMxbHmzDlr4.UMxbHmzDlr4'
 
    Gets the Chat message specified
    #>

    [OutputType('Google.Apis.HangoutsChat.v1.Data.Message')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [Alias('Id')]
        [string[]]
        $Name
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/chat.bot'
            ServiceType = 'Google.Apis.HangoutsChat.v1.HangoutsChatService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($msg in $Name) {
            try {
                $request = $service.Spaces.Messages.Get($msg)
                Write-Verbose "Getting Message '$msg'"
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSChatMessage'
function Get-GSChatSpace {
    <#
    .SYNOPSIS
    Gets a Chat space
 
    .DESCRIPTION
    Gets a Chat space
 
    .PARAMETER Space
    The resource name of the space for which membership list is to be fetched, in the form "spaces".
 
    If left blank, returns the list of spaces the bot is a member of
 
    Example: spaces/AAAAMpdlehY
 
    .EXAMPLE
    Get-GSChatSpace
 
    Gets the list of Chat spaces the bot is a member of
    #>

    [OutputType('Google.Apis.HangoutsChat.v1.Data.Space')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("Name")]
        [string[]]
        $Space
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/chat.bot'
            ServiceType = 'Google.Apis.HangoutsChat.v1.HangoutsChatService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        if ($Space) {
            foreach ($sp in $Space) {
                try {
                    if ($sp -notlike "spaces/*") {
                        try {
                            $sp = Get-GSChatConfig -SpaceName $sp -ErrorAction Stop
                        }
                        catch {
                            $sp = "spaces/$sp"
                        }
                    }
                    $request = $service.Spaces.Get($sp)
                    Write-Verbose "Getting Chat Space '$sp'"
                    $request.Execute()
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
        else {
            try {
                $spaceArray = @()
                $request = $service.Spaces.List()
                Write-Verbose "Getting Chat Space List"
                [int]$i = 1
                do {
                    $result = $request.Execute()
                    $result.Spaces
                    $spaceArray += $result.Spaces
                    if ($result.NextPageToken) {
                        $request.PageToken = $result.NextPageToken
                    }
                    [int]$retrieved = ($i + $result.Spaces.Count) - 1
                    Write-Verbose "Retrieved $retrieved Spaces..."
                    [int]$i = $i + $result.Spaces.Count
                }
                until (!$result.NextPageToken)
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
    End {
        Write-Verbose "Updating PSGSuite Config with Space list"
        $spaceHashArray = @()
        $spaceArray | ForEach-Object {
            if ($_.DisplayName) {
                $spaceHashArray += @{$_.DisplayName = $_.Name}

            }
            else {
                $member = Get-GSChatMember -Space $_.Name -Verbose:$false
                $id = $member.Member.Name
                $primaryEmail = (Get-GSUser -User ($id.Replace('users/',''))).PrimaryEmail
                $spaceHashArray += @{
                    $id = $_.Name
                    $member.Member.DisplayName = $_.Name
                    $primaryEmail = $_.Name
                }
            }
        }
        Set-PSGSuiteConfig -Space $spaceHashArray -Verbose:$false
    }
}

Export-ModuleMember -Function 'Get-GSChatSpace'
function Remove-GSChatMessage {
    <#
    .SYNOPSIS
    Removes a Chat message
     
    .DESCRIPTION
    Removes a Chat message
 
    .PARAMETER Name
    Resource name of the message to be removed, in the form "spaces/messages".
 
    Example: spaces/AAAAMpdlehY/messages/UMxbHmzDlr4.UMxbHmzDlr4
     
    .EXAMPLE
    Remove-GSChatMessage -Name 'spaces/AAAAMpdlehY/messages/UMxbHmzDlr4.UMxbHmzDlr4'
 
    Removes the Chat message specified after confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [Alias('Id')]
        [string[]]
        $Name
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/chat.bot'
            ServiceType = 'Google.Apis.HangoutsChat.v1.HangoutsChatService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($msg in $Name) {
            try {
                if ($PSCmdlet.ShouldProcess("Removing Message '$msg'")) {
                    $request = $service.Spaces.Messages.Delete($msg)
                    $request.Execute()
                    Write-Verbose "Successfully removed Message '$msg'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSChatMessage'
function Send-GSChatMessage {
    <#
    .SYNOPSIS
    Sends a Chat message
 
    .DESCRIPTION
    Sends a Chat message
 
    .PARAMETER Text
    Plain-text body of the message.
 
    .PARAMETER Thread
    The thread the message belongs to, in the form "spaces/threads".
 
    Example: spaces/AAAA3dnRkmI/threads/_EyIp5BthJk
 
    .PARAMETER FallbackText
    A plain-text description of the message's cards, used when the actual cards cannot be displayed (e.g. mobile notifications).
 
    .PARAMETER PreviewText
    Text for generating preview chips. This text will not be displayed to the user, but any links to images, web pages, videos, etc. included here will generate preview chips.
 
    .PARAMETER ActionResponseType
    Part of the ActionResponse. Parameters that a bot can use to configure how its response is posted.
 
    The ActionResponseType is the type of bot response.
 
    Available values are:
    * NEW_MESSAGE: Post as a new message in the topic.
    * UPDATE_MESSAGE: Update the bot's own message. (Only after CARD_CLICKED events.)
    * REQUEST_CONFIG: Privately ask the user for additional auth or config.
 
    .PARAMETER ActionResponseUrl
    Part of the ActionResponse. Parameters that a bot can use to configure how its response is posted.
 
    The ActionResponseUrl is the URL for users to auth or config. (Only for REQUEST_CONFIG response types.)
 
    .PARAMETER Parent
    The resource name of the space to send the message to, in the form "spaces".
 
    Example: spaces/AAAAMpdlehY
 
    .PARAMETER ThreadKey
    Opaque thread identifier string that can be specified to group messages into a single thread. If this is the first message with a given thread identifier, a new thread is created. Subsequent messages with the same thread identifier will be posted into the same thread. This relieves bots and webhooks from having to store the Hangouts Chat thread ID of a thread (created earlier by them) to post further updates to it.
 
    .PARAMETER RestParent
    The resource name of the space to send the message to via REST API call, in the form "spaces".
 
    Example: spaces/AAAAMpdlehY
 
    .PARAMETER Webhook
    The Url of the Webhook for the space to send the message to.
 
    You can safely store an encrypted dictionary of Webhooks in the PSGSuite Config by passing a hashtable to the `-Webhook` parameter, i.e.:
        Set-PSGSuiteConfig -Webhook @{JobReports = 'https://chat.googleapis.com/v1/spaces/xxxxxxxxxx/messages?key=xxxxxxxxxxxxxxxxxx&token=xxxxxxxxxxxxxxxxxx'}
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    If section widgets are passed directly to this function, a new section without a SectionHeader will be created and the widgets will be added to it
 
    .PARAMETER BodyPassThru
    If $true, returns the message body.
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook JobReports
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    [OutputType('Google.Apis.HangoutsChat.v1.Data.Message')]
    [cmdletbinding(DefaultParameterSetName = "Webhook")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [string[]]
        $Text,
        [parameter(Mandatory = $false)]
        [string]
        $Thread,
        [parameter(Mandatory = $false)]
        [string[]]
        $FallbackText,
        [parameter(Mandatory = $false)]
        [string]
        $PreviewText,
        [parameter(Mandatory = $false)]
        [ValidateSet('NEW_MESSAGE','UPDATE_MESSAGE','REQUEST_CONFIG')]
        [string]
        $ActionResponseType,
        [parameter(Mandatory = $false)]
        [string]
        $ActionResponseUrl,
        [parameter(Mandatory = $true,ParameterSetName = "SDK")]
        [string[]]
        $Parent,
        [parameter(Mandatory = $false,ParameterSetName = "SDK")]
        [parameter(Mandatory = $false,ParameterSetName = "Rest")]
        [string]
        $ThreadKey,
        [parameter(Mandatory = $true,ParameterSetName = "Rest")]
        [string[]]
        $RestParent,
        [parameter(Mandatory = $true,ParameterSetName = "Webhook")]
        [string[]]
        $Webhook,
        [parameter(Mandatory = $false,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment,
        [parameter(Mandatory = $true,ParameterSetName = "BodyPassThru")]
        [switch]
        $BodyPassThru
    )
    Begin {
        $addlSections = @()
        $addlCardActions = @()
        $addlSectionWidgets = @()
        switch ($PSCmdlet.ParameterSetName) {
            default {
                $body = @{}
                foreach ($key in $PSBoundParameters.Keys) {
                    switch ($key) {
                        Text {
                            $body['text'] = ($Text -join "`n")
                        }
                        PreviewText {
                            $body['previewText'] = $PSBoundParameters[$key]
                        }
                        FallbackText {
                            $body['fallbackText'] = ($PSBoundParameters[$key] -join "`n")
                        }
                        Thread {
                            $body['thread'] = @{
                                name = $Thread
                            }
                        }
                        ActionResponseType {
                            if (!$body['actionResponse']) {
                                $body['actionResponse'] = @{}
                            }
                            $body['actionResponse']['type'] = $PSBoundParameters[$key]
                        }
                        ActionResponseUrl {
                            if (!$body['actionResponse']) {
                                $body['actionResponse'] = @{}
                            }
                            $body['actionResponse']['url'] = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            SDK {
                $serviceParams = @{
                    Scope       = 'https://www.googleapis.com/auth/chat.bot'
                    ServiceType = 'Google.Apis.HangoutsChat.v1.HangoutsChatService'
                }
                $service = New-GoogleService @serviceParams
                $body = New-Object 'Google.Apis.HangoutsChat.v1.Data.Message'
                foreach ($key in $PSBoundParameters.Keys) {
                    switch ($key) {
                        Text {
                            $body.Text = ($PSBoundParameters[$key] -join "`n")
                        }
                        PreviewText {
                            $body.PreviewText = $PSBoundParameters[$key]
                        }
                        FallbackText {
                            $body.FallbackText = ($PSBoundParameters[$key] -join "`n")
                        }
                        Thread {
                            $body.Thread = New-Object 'Google.Apis.HangoutsChat.v1.Data.Thread' -Property @{
                                Name = $Thread
                            }
                        }
                        ActionResponseType {
                            if (!$body.ActionResponse) {
                                $body.ActionResponse = New-Object 'Google.Apis.HangoutsChat.v1.Data.ActionResponse'
                            }
                            $body.ActionResponse.Type = $PSBoundParameters[$key]
                        }
                        ActionResponseUrl {
                            if (!$body.ActionResponse) {
                                $body.ActionResponse = New-Object 'Google.Apis.HangoutsChat.v1.Data.ActionResponse'
                            }
                            $body.ActionResponse.Url = $PSBoundParameters[$key]
                        }
                    }
                }
            }
        }
    }
    Process {
        foreach ($segment in $MessageSegment) {
            switch -RegEx ($segment['SDK'].PSTypeNames[0]) {
                '(.*?)Google\.Apis\.HangoutsChat\.v1\.Data\.Card' {
                    switch ($PSCmdlet.ParameterSetName) {
                        default {
                            if (!$body['cards']) {
                                $body['cards'] = @()
                            }
                            $body['cards'] += $segment['Webhook']
                        }
                        SDK {
                            if (!$body.Cards) {
                                $body.Cards = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Card]'
                            }
                            $body.Cards.Add($segment['SDK']) | Out-Null
                        }
                    }
                }
                '(.*?)Google\.Apis\.HangoutsChat\.v1\.Data\.Section' {
                    $addlSections += $segment
                }
                '(.*?)Google\.Apis\.HangoutsChat\.v1\.Data\.CardAction' {
                    $addlCardActions += $segment
                }
                default {
                    Write-Verbose "Matched a $($segment['SDK'].PSTypeNames[0]) in the MessageSegments!"
                    $addlSectionWidgets += $segment
                }
            }
        }
    }
    End {
        switch ($PSCmdlet.ParameterSetName) {
            default {
                if ($addlCardActions -or $addlSections -or $addlSectionWidgets) {
                    if (!$body['cards']) {
                        $cardless = $true
                        $body['cards'] = @()
                    }
                    if ($addlSections) {
                        $body['cards'] += ($addlSections | Add-GSChatCard)['Webhook']
                        $cardless = $false
                    }
                    if ($addlSectionWidgets) {
                        if ($cardless) {
                            $body['cards'] += ($addlSectionWidgets | Add-GSChatCardSection | Add-GSChatCard)['Webhook']
                        }
                        else {
                            $newSection = ($addlSectionWidgets | Add-GSChatCardSection)['Webhook']
                            if (!$body['cards'][-1]['sections']) {
                                $body['cards'][-1]['sections'] = @()
                            }
                            $body['cards'][-1]['sections'] += $newSection
                        }
                        $cardless = $false
                    }
                    if ($addlCardActions) {
                        if ($cardless) {
                            $body['cards'] += ($addlCardActions | Add-GSChatCard)['Webhook']
                        }
                        elseif (!$body['cards'][-1]['cardActions']) {
                            $body['cards'][-1]['cardActions'] = @()
                        }
                        foreach ($cardAction in $addlCardActions) {
                            $body['cards'][-1]['cardActions'] += $cardAction['Webhook']
                        }
                    }
                }
                switch ($PSCmdlet.ParameterSetName) {
                    Webhook {
                        $body = $body | ConvertTo-Json -Depth 15
                        foreach ($hook in $Webhook) {
                            try {
                                if ($hook -notlike "https://chat.googleapis.com/v1/spaces/*") {
                                    $hook = Get-GSChatConfig -WebhookName $hook -ErrorAction Stop
                                }
                                Write-Verbose "Sending Chat Message via Webhook to '$($hook -replace "\?key\=.*",'')'"
                                Invoke-RestMethod -Method Post -Uri ([Uri]$hook) -Body $body -ContentType 'application/json' -Verbose:$false | Add-Member -MemberType NoteProperty -Name 'Webhook' -Value $hook -PassThru
                            }
                            catch {
                                if ($ErrorActionPreference -eq 'Stop') {
                                    $PSCmdlet.ThrowTerminatingError($_)
                                }
                                else {
                                    Write-Error $_
                                }
                            }
                        }
                    }
                    Rest {
                        $body = $body | ConvertTo-Json -Depth 15
                        foreach ($restPar in $RestParent) {
                            try {
                                if ($restPar -notlike "spaces/*") {
                                    try {
                                        $restPar = Get-GSChatConfig -SpaceName $restPar -ErrorAction Stop
                                    }
                                    catch {
                                        $restPar = "spaces/$restPar"
                                    }
                                }
                                $header = @{
                                    Authorization = "Bearer $(Get-GSToken -Scopes "https://www.googleapis.com/auth/chat.bot" -Verbose:$false)"
                                }
                                $hook = "https://chat.googleapis.com/v1/$($restPar)/messages"
                                if ($PSBoundParameters.Keys -contains 'ThreadKey') {
                                    $hook = "$($hook)?threadKey=$ThreadKey"
                                    $addlText = " in ThreadKey '$ThreadKey'"
                                }
                                else {
                                    $addlText = ""
                                }
                                Write-Verbose "Sending Chat Message via REST API to parent '$restPar'$addlText"
                                Invoke-RestMethod -Method Post -Uri ([Uri]$hook) -Headers $header -Body $body -ContentType 'application/json' -Verbose:$false | Add-Member -MemberType NoteProperty -Name 'RestParent' -Value $restPar -PassThru
                            }
                            catch {
                                if ($ErrorActionPreference -eq 'Stop') {
                                    $PSCmdlet.ThrowTerminatingError($_)
                                }
                                else {
                                    Write-Error $_
                                }
                            }
                        }
                    }
                    BodyPassThru {
                        $newBody = @{
                            token = (Get-GSToken -Scopes "https://www.googleapis.com/auth/chat.bot" -Verbose:$false)
                            body = $body
                        }
                        $newBody = $newBody | ConvertTo-Json -Depth 20 -Compress
                        return $newBody
                    }
                }
            }
            SDK {
                if ($addlCardActions -or $addlSections -or $addlSectionWidgets) {
                    if (!$body.Cards) {
                        $cardless = $true
                        $body.Cards = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Card]'
                    }
                    if ($addlSections) {
                        $body.Cards.Add(($addlSections | Add-GSChatCard)['SDK']) | Out-Null
                        $cardless = $false
                    }
                    if ($addlSectionWidgets) {
                        if ($cardless) {
                            $body.Cards.Add(($addlSectionWidgets | Add-GSChatCardSection | Add-GSChatCard)['SDK']) | Out-Null
                        }
                        else {
                            $newSection = ($addlSectionWidgets | Add-GSChatCardSection)['SDK']
                            if (!$body.Cards[-1].Sections) {
                                $body.Cards[-1].Sections = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Section]'
                            }
                            $body.Cards[-1].Sections.Add($newSection) | Out-Null
                        }
                        $cardless = $false
                    }
                    if ($addlCardActions) {
                        if ($cardless) {
                            $body.Cards.Add(($addlCardActions | Add-GSChatCard)['SDK'])
                        }
                        elseif (!$body.Cards[-1].CardActions) {
                            $body.Cards[-1].CardActions = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.CardAction]'
                        }
                        foreach ($cardAction in $addlCardActions) {
                            $body.Cards[-1].CardActions.Add($cardAction['SDK']) | Out-Null
                        }
                    }
                }
                foreach ($par in $Parent){
                    try {
                        if ($par -notlike "spaces/*") {
                            try {
                                $par = Get-GSChatConfig -SpaceName $par -ErrorAction Stop
                            }
                            catch {
                                $par = "spaces/$par"
                            }
                        }
                        $request = $service.Spaces.Messages.Create($body,$par)
                        if ($PSBoundParameters.Keys -contains 'ThreadKey') {
                            $request.ThreadKey = $ThreadKey
                            $addlText = " in ThreadKey '$ThreadKey'"
                        }
                        else {
                            $addlText = ""
                        }
                        Write-Verbose "Sending Chat Message via SDK to Space '$par'$addlText"
                        $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Parent' -Value $par -PassThru | Add-Member -MemberType NoteProperty -Name 'ThreadKey' -Value $ThreadKey -PassThru
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Send-GSChatMessage'
function Update-GSChatMessage {
    <#
    .SYNOPSIS
    Updates a Chat message, i.e. for a CardClicked response
 
    .DESCRIPTION
    Updates a Chat message, i.e. for a CardClicked response
 
    .PARAMETER MessageId
    Resource name, in the form "spaces/messages".
 
    Example: spaces/89L51AAAAAE/messages/kbZTbcol8H4.kbZTbcol8H4
 
    .PARAMETER UpdateMask
    Required. The field paths to be updated.
 
    Currently supported field paths: "text", "cards".
 
    .PARAMETER Text
    Plain-text body of the message.
 
    .PARAMETER FallbackText
    A plain-text description of the message's cards, used when the actual cards cannot be displayed (e.g. mobile notifications).
 
    .PARAMETER PreviewText
    Text for generating preview chips. This text will not be displayed to the user, but any links to images, web pages, videos, etc. included here will generate preview chips.
 
    .PARAMETER ActionResponseType
    Part of the ActionResponse. Parameters that a bot can use to configure how its response is posted.
 
    The ActionResponseType is the type of bot response.
 
    Available values are:
    * NEW_MESSAGE: Post as a new message in the topic.
    * UPDATE_MESSAGE: Update the bot's own message. (Only after CARD_CLICKED events.)
    * REQUEST_CONFIG: Privately ask the user for additional auth or config.
 
    .PARAMETER ActionResponseUrl
    Part of the ActionResponse. Parameters that a bot can use to configure how its response is posted.
 
    The ActionResponseUrl is the URL for users to auth or config. (Only for REQUEST_CONFIG response types.)
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    If section widgets are passed directly to this function, a new section without a SectionHeader will be created and the widgets will be added to it
 
    .PARAMETER BodyPassThru
    If $true, returns the message body.
 
    .PARAMETER UseRest
    If $true, uses the REST API.
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    [OutputType('Google.Apis.HangoutsChat.v1.Data.Message')]
    [cmdletbinding(DefaultParameterSetName = "Update")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ParameterSetName = "Update")]
        [string]
        $MessageId,
        [parameter(Mandatory = $true,ParameterSetName = "BodyPassThru")]
        [switch]
        $BodyPassThru,
        [parameter(Mandatory = $false,Position = 1)]
        [ValidateSet("text","cards")]
        [string[]]
        $UpdateMask,
        [parameter(Mandatory = $false)]
        [string[]]
        $Text,
        [parameter(Mandatory = $false)]
        [string[]]
        $FallbackText,
        [parameter(Mandatory = $false)]
        [string]
        $PreviewText,
        [parameter(Mandatory = $false)]
        [ValidateSet('NEW_MESSAGE','UPDATE_MESSAGE','REQUEST_CONFIG')]
        [string]
        $ActionResponseType = 'UPDATE_MESSAGE',
        [parameter(Mandatory = $false)]
        [string]
        $ActionResponseUrl,
        [parameter(Mandatory = $false,ParameterSetName = "Update")]
        [switch]
        $UseRest,
        [parameter(Mandatory = $false,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $addlSections = @()
        $addlCardActions = @()
        $addlSectionWidgets = @()
        if ($UseRest -or $BodyPassThru) {
            $body = @{
                actionResponse = @{
                    type = $ActionResponseType
                }
            }
            foreach ($key in $PSBoundParameters.Keys) {
                switch ($key) {
                    Text {
                        if ($UpdateMask -notcontains 'text') {
                            $UpdateMask += 'text'
                        }
                        $body['text'] = ($Text -join "`n")
                    }
                    PreviewText {
                        $body['previewText'] = $PSBoundParameters[$key]
                    }
                    FallbackText {
                        $body['fallbackText'] = ($PSBoundParameters[$key] -join "`n")
                    }
                    ActionResponseUrl {
                        if (!$body['actionResponse']) {
                            $body['actionResponse'] = @{}
                        }
                        $body['actionResponse']['url'] = $PSBoundParameters[$key]
                    }
                }
            }
        }
        else {
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/chat.bot'
                ServiceType = 'Google.Apis.HangoutsChat.v1.HangoutsChatService'
            }
            $service = New-GoogleService @serviceParams
            $body = New-Object 'Google.Apis.HangoutsChat.v1.Data.Message'
            $body.ActionResponse = New-Object 'Google.Apis.HangoutsChat.v1.Data.ActionResponse'
            $body.ActionResponse.Type = $ActionResponseType
            foreach ($key in $PSBoundParameters.Keys) {
                switch ($key) {
                    Text {
                        if ($UpdateMask -notcontains 'text') {
                            $UpdateMask += 'text'
                        }
                        $body.Text = ($PSBoundParameters[$key] -join "`n")
                    }
                    PreviewText {
                        $body.PreviewText = $PSBoundParameters[$key]
                    }
                    FallbackText {
                        $body.FallbackText = ($PSBoundParameters[$key] -join "`n")
                    }
                    ActionResponseUrl {
                        if (!$body.ActionResponse) {
                            $body.ActionResponse = New-Object 'Google.Apis.HangoutsChat.v1.Data.ActionResponse'
                        }
                        $body.ActionResponse.Url = $PSBoundParameters[$key]
                    }
                }
            }
        }
    }
    Process {
        if ($MessageSegment) {
            if ($UpdateMask -notcontains 'cards') {
                $UpdateMask += 'cards'
            }
            foreach ($segment in $MessageSegment) {
                switch -RegEx ($segment['SDK'].PSTypeNames[0]) {
                    '(.*?)Google\.Apis\.HangoutsChat\.v1\.Data\.Card' {
                        if ($UseRest -or $BodyPassThru) {
                            if (!$body['cards']) {
                                $body['cards'] = @()
                            }
                            $body['cards'] += $segment['Webhook']
                        }
                        else {
                            if (!$body.Cards) {
                                $body.Cards = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Card]'
                            }
                            $body.Cards.Add($segment['SDK']) | Out-Null
                        }
                    }
                    '(.*?)Google\.Apis\.HangoutsChat\.v1\.Data\.Section' {
                        $addlSections += $segment
                    }
                    '(.*?)Google\.Apis\.HangoutsChat\.v1\.Data\.CardAction' {
                        $addlCardActions += $segment
                    }
                    default {
                        Write-Verbose "Matched a $($segment['SDK'].PSTypeNames[0]) in the MessageSegments!"
                        $addlSectionWidgets += $segment
                    }
                }
            }
        }
    }
    End {
        if ($UseRest -or $BodyPassThru) {
            if ($addlCardActions -or $addlSections -or $addlSectionWidgets) {
                if (!$body['cards']) {
                    $cardless = $true
                    $body['cards'] = @()
                }
                if ($addlSections) {
                    $body['cards'] += ($addlSections | Add-GSChatCard)['Webhook']
                    $cardless = $false
                }
                if ($addlSectionWidgets) {
                    if ($cardless) {
                        $body['cards'] += ($addlSectionWidgets | Add-GSChatCardSection | Add-GSChatCard)['Webhook']
                    }
                    else {
                        $newSection = ($addlSectionWidgets | Add-GSChatCardSection)['Webhook']
                        if (!$body['cards'][-1]['sections']) {
                            $body['cards'][-1]['sections'] = @()
                        }
                        $body['cards'][-1]['sections'] += $newSection
                    }
                    $cardless = $false
                }
                if ($addlCardActions) {
                    if ($cardless) {
                        $body['cards'] += ($addlCardActions | Add-GSChatCard)['Webhook']
                    }
                    elseif (!$body['cards'][-1]['cardActions']) {
                        $body['cards'][-1]['cardActions'] = @()
                    }
                    foreach ($cardAction in $addlCardActions) {
                        $body['cards'][-1]['cardActions'] += $cardAction['Webhook']
                    }
                }
            }
            if ($BodyPassThru) {
                $newBody = @{
                    token = (Get-GSToken -Scopes "https://www.googleapis.com/auth/chat.bot" -Verbose:$false)
                    body = $body
                    updateMask = ($UpdateMask -join ',')
                }
                $newBody = $newBody | ConvertTo-Json -Depth 20 -Compress
                return $newBody
            }
            else {
                $body = $body | ConvertTo-Json -Depth 20
                try {
                    $header = @{
                        Authorization = "Bearer $(Get-GSToken -P12KeyPath $Script:PSGSuite.P12KeyPath -Scopes "https://www.googleapis.com/auth/chat.bot" -AppEmail $Script:PSGSuite.AppEmail -AdminEmail $Script:PSGSuite.AdminEmail -Verbose:$false)"
                    }
                    $hook = "https://chat.googleapis.com/v1/$($MessageId)?updateMask=$($UpdateMask -join ',')"
                    Write-Verbose "Updating Chat Message via REST API to parent '$MessageId'"
                    Invoke-RestMethod -Method Put -Uri ([Uri]$hook) -Headers $header -Body $body -ContentType 'application/json' -Verbose:$false | Add-Member -MemberType NoteProperty -Name 'MessageId' -Value $MessageId -PassThru
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
        else {
            if ($addlCardActions -or $addlSections -or $addlSectionWidgets) {
                if (!$body.Cards) {
                    $cardless = $true
                    $body.Cards = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Card]'
                }
                if ($addlSections) {
                    $body.Cards.Add(($addlSections | Add-GSChatCard)['SDK']) | Out-Null
                    $cardless = $false
                }
                if ($addlSectionWidgets) {
                    if ($cardless) {
                        $body.Cards.Add(($addlSectionWidgets | Add-GSChatCardSection | Add-GSChatCard)['SDK']) | Out-Null
                    }
                    else {
                        $newSection = ($addlSectionWidgets | Add-GSChatCardSection)['SDK']
                        if (!$body.Cards[-1].Sections) {
                            $body.Cards[-1].Sections = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Section]'
                        }
                        $body.Cards[-1].Sections.Add($newSection) | Out-Null
                    }
                    $cardless = $false
                }
                if ($addlCardActions) {
                    if ($cardless) {
                        $body.Cards.Add(($addlCardActions | Add-GSChatCard)['SDK'])
                    }
                    elseif (!$body.Cards[-1].CardActions) {
                        $body.Cards[-1].CardActions = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.CardAction]'
                    }
                    foreach ($cardAction in $addlCardActions) {
                        $body.Cards[-1].CardActions.Add($cardAction['SDK']) | Out-Null
                    }
                }
            }
            try {
                $request = $service.Spaces.Messages.Update($body,$MessageId)
                $request.UpdateMask = $UpdateMask
                Write-Verbose "Updating Chat Message Id '$MessageId'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'MessageId' -Value $MessageId -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSChatMessage'
function Add-GSCourseParticipant {
    <#
    .SYNOPSIS
    Adds students and/or teachers to a course
 
    .DESCRIPTION
    Adds students and/or teachers to a course
 
    .PARAMETER CourseId
    Identifier of the course to add participants to. This identifier can be either the Classroom-assigned identifier or an alias.
 
    .PARAMETER Student
    Identifier of the user.
 
    This identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER Teacher
    Identifier of the user.
 
    This identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Add-GSCourseParticipant -CourseId 'architecture-101' -Student plato@athens.edu,aristotle@athens.edu -Teacher zeus@athens.edu
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.Student')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [String]
        $CourseId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('PrimaryEmail','Email','Mail')]
        [String[]]
        $Student,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String[]]
        $Teacher,
        [parameter(Mandatory = $false)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.rosters'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($part in $Student | Where-Object {-not [String]::IsNullOrEmpty($_)}) {
            try {
                $body = New-Object 'Google.Apis.Classroom.v1.Data.Student'
                if ( -not ($part -as [decimal])) {
                    if ($part -ceq 'me') {
                        $part = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($part -notlike "*@*.*") {
                        $part = "$($part)@$($Script:PSGSuite.Domain)"
                    }
                }
                $body.UserId = $part
                Write-Verbose "Adding Student '$part' to Course '$CourseId'"
                $request = $service.Courses.Students.Create($body,$CourseId)
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
        foreach ($part in $Teacher | Where-Object {-not [String]::IsNullOrEmpty($_)}) {
            try {
                $body = New-Object 'Google.Apis.Classroom.v1.Data.Teacher'
                try {
                    [decimal]$part | Out-Null
                }
                catch {
                    if ($part -ceq 'me') {
                        $part = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($part -notlike "*@*.*") {
                        $part = "$($part)@$($Script:PSGSuite.Domain)"
                    }
                }
                $body.UserId = $part
                Write-Verbose "Adding Teacher '$part' to Course '$CourseId'"
                $request = $service.Courses.Teachers.Create($body,$CourseId)
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSCourseParticipant'
function Confirm-GSCourseInvitation {
    <#
    .SYNOPSIS
    Accepts an invitation, removing it and adding the invited user to the teachers or students (as appropriate) of the specified course. Only the invited user may accept an invitation.
 
    .DESCRIPTION
    Accepts an invitation, removing it and adding the invited user to the teachers or students (as appropriate) of the specified course. Only the invited user may accept an invitation.
 
    .PARAMETER Id
    Identifier of the invitation to accept.
 
    .PARAMETER User
    Email or email name part of the invited user.
 
    .EXAMPLE
    Confirm-GSCourseInvitation -Id $inviteId -User aristotle@athens.edu
    #>

    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [String]
        $Id,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.rosters'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            Write-Verbose "Accepting Invitation '$Id' for user '$User'"
            $request = $service.Invitations.Accept($Id)
            $request.Execute()
            Write-Verbose "The Invitation has been successfully accepted for user '$User'"
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Confirm-GSCourseInvitation'
function Get-GSClassroomUserProfile {
    <#
    .SYNOPSIS
    Gets a classroom user profile
 
    .DESCRIPTION
    Gets a classroom user profile
 
    .PARAMETER UserId
    Identifier of the profile to return. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER Fields
    The specific fields to fetch
 
    .EXAMPLE
    Get-GSClassroomUserProfile -UserId aristotle@athens.edu
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.UserProfile')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id','PrimaryEmail','Mail','UserKey')]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $UserId,
        [parameter(Mandatory = $false)]
        [String[]]
        $Fields = '*'
    )
    Begin {
        $serviceParams = @{
            Scope       = @(
                'https://www.googleapis.com/auth/classroom.rosters'
                'https://www.googleapis.com/auth/classroom.profile.emails'
                'https://www.googleapis.com/auth/classroom.profile.photos'
            )
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($part in $UserId) {
            try {
                if ( -not ($part -as [decimal])) {
                    if ($part -ceq 'me') {
                        $part = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($part -notlike "*@*.*") {
                        $part = "$($part)@$($Script:PSGSuite.Domain)"
                    }
                }
                Write-Verbose "Getting Classroom User Profile for '$part'"
                $request = $service.UserProfiles.Get($part)
                $request.Fields = "$($Fields -join ",")"
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSClassroomUserProfile'
function Get-GSCourse {
    <#
    .SYNOPSIS
    Gets a classroom course or list of courses
 
    .DESCRIPTION
    Gets a classroom course or list of courses
 
    .PARAMETER Id
    Identifier of the course to return. This identifier can be either the Classroom-assigned identifier or an alias.
 
    If excluded, returns the list of courses.
 
    .PARAMETER Teacher
    Restricts returned courses to those having a teacher with the specified identifier. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER Student
    Restricts returned courses to those having a student with the specified identifier. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER CourseStates
    Restricts returned courses to those in one of the specified states.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Get-GSCourse -Teacher aristotle@athens.edu
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.Course')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ParameterSetName = "Get")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Id,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [String]
        $Teacher,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [String]
        $Student,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Google.Apis.Classroom.v1.CoursesResource+ListRequest+CourseStatesEnum[]]
        $CourseStates,
        [parameter(Mandatory = $false)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.courses'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        if ($PSBoundParameters.Keys -contains 'Id') {
            foreach ($I in $Id) {
                try {
                    Write-Verbose "Getting Course '$Id'"
                    $request = $service.Courses.Get($Id)
                    $request.Execute()
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
        else {
            try {
                Write-Verbose "Getting Course List"
                $request = $service.Courses.List()
                foreach ($s in $CourseStates) {
                    $request.CourseStates += $s
                }
                if ($PSBoundParameters.Keys -contains 'Student') {
                    $request.StudentId = $PSBoundParameters['Student']
                }
                if ($PSBoundParameters.Keys -contains 'Teacher') {
                    $request.TeacherId = $PSBoundParameters['Teacher']
                }
                [int]$i = 1
                do {
                    $result = $request.Execute()
                    if ($null -ne $result.Courses) {
                        $result.Courses
                    }
                    $request.PageToken = $result.NextPageToken
                    [int]$retrieved = ($i + $result.Courses.Count) - 1
                    Write-Verbose "Retrieved $retrieved Courses..."
                    [int]$i = $i + $result.Courses.Count
                }
                until (!$result.NextPageToken)
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCourse'
function Get-GSCourseAlias {
    <#
    .SYNOPSIS
    Gets the list of aliases for a course.
 
    .DESCRIPTION
    Gets the list of aliases for a course.
 
    .PARAMETER CourseId
    Identifier of the course to alias. This identifier can be either the Classroom-assigned identifier or an alias.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Get-GSCourseAlias -CourseId 'architecture-101'
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.CourseAlias')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $CourseId,
        [parameter(Mandatory = $false)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.courses'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Getting Alias list for Course '$CourseId'"
            $request = $service.Courses.Aliases.List($CourseId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCourseAlias'
function Get-GSCourseInvitation {
    <#
    .SYNOPSIS
    Gets a course invitation or list of invitations
 
    .DESCRIPTION
    Gets a course invitation or list of invitations
 
    .PARAMETER Id
    Identifier of the invitation to return.
 
    .PARAMETER CourseId
    Restricts returned invitations to those for a course with the specified identifier. This identifier can be either the Classroom-assigned identifier or an alias.
 
    .PARAMETER UserId
    Restricts returned invitations to those for a specific user. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Get-GSCourseInvitation -CourseId philosophy-101
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.Invitation')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,ParameterSetName = "Get")]
        [String[]]
        $Id,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [String]
        $CourseId,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [String]
        $UserId,
        [parameter(Mandatory = $false)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.rosters'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($part in $Id) {
                    try {
                        Write-Verbose "Getting Invitation ID '$part'"
                        $request = $service.Invitations.Get($part)
                        $request.Execute()
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            List {
                try {
                    if ($PSBoundParameters.Keys -notcontains 'CourseId' -and $PSBoundParameters.Keys -notcontains 'UserId') {
                        Write-Error "You must specify a CourseId and/or a UserId!"
                    }
                    else {
                        $request =  $service.Invitations.List()
                        $verbMsg = ""
                        if ($PSBoundParameters.Keys -contains 'CourseId') {
                            $verbMsg += " [Course: $CourseId]"
                            $request.CourseId = $CourseId
                        }
                        if ($PSBoundParameters.Keys -contains 'UserId') {
                            if ( -not ($UserId -as [decimal])) {
                                if ($UserId -ceq 'me') {
                                    $UserId = $Script:PSGSuite.AdminEmail
                                }
                                elseif ($UserId -notlike "*@*.*") {
                                    $UserId = "$($UserId)@$($Script:PSGSuite.Domain)"
                                }
                            }
                            $verbMsg += " [User: $UserId]"
                            $request.UserId = $UserId
                        }
                        Write-Verbose "Getting List of Invitations for$($verbMsg)"
                        [int]$retrieved = 0
                        [int]$i = 1
                        do {
                            $result = $request.Execute()
                            if ($null -ne $result.Invitations) {
                                $result.Invitations
                            }
                            [int]$retrieved = ($i + $result.Invitations.Count) - 1
                            [int]$i = $i + $result.Invitations.Count
                            $request.PageToken = $result.NextPageToken
                            Write-Verbose "Retrieved $retrieved Invitations..."
                        }
                        until (!$result.NextPageToken)
                    }
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCourseInvitation'
function Get-GSCourseParticipant {
    <#
    .SYNOPSIS
    Gets a course participant or list of participants (teachers/students)
 
    .DESCRIPTION
    Gets a course participant or list of participants (teachers/students)
 
    .PARAMETER CourseId
    Identifier of the course to get participants of. This identifier can be either the Classroom-assigned identifier or an alias.
 
    .PARAMETER Role
    The Role for which you would like to list participants for.
 
    Available values are:
 
    * Student
    * Teacher
 
    The default value for this parameter is @('Teacher','Student')
 
    .PARAMETER Teacher
    Restricts returned courses to those having a teacher with the specified identifier. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER Student
    Restricts returned courses to those having a student with the specified identifier. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER User
    The user to authenticate the request as
 
    .PARAMETER Fields
    The specific fields to fetch
 
    .EXAMPLE
    Get-GSCourseParticipant -Teacher aristotle@athens.edu
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.Student')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [alias('Id')]
        [String]
        $CourseId,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateSet('Teacher','Student')]
        [String[]]
        $Role = @('Teacher','Student'),
        [parameter(Mandatory = $false,ParameterSetName = "Get")]
        [String[]]
        $Teacher,
        [parameter(Mandatory = $false,ParameterSetName = "Get")]
        [String[]]
        $Student,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [String[]]
        $Fields = '*'
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = @(
                'https://www.googleapis.com/auth/classroom.rosters'
                'https://www.googleapis.com/auth/classroom.profile.emails'
                'https://www.googleapis.com/auth/classroom.profile.photos'
            )
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($part in $Student) {
                    try {
                        if ( -not ($part -as [decimal])) {
                            if ($part -ceq 'me') {
                                $part = $Script:PSGSuite.AdminEmail
                            }
                            elseif ($part -notlike "*@*.*") {
                                $part = "$($part)@$($Script:PSGSuite.Domain)"
                            }
                        }
                        Write-Verbose "Getting Student '$part' for Course '$CourseId'"
                        $request = $service.Courses.Students.Get($CourseId,$part)
                        $request.Fields = "$($Fields -join ",")"
                        $request.Execute()
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
                foreach ($part in $Teacher) {
                    try {
                        try {
                            [decimal]$part | Out-Null
                        }
                        catch {
                            if ($part -ceq 'me') {
                                $part = $Script:PSGSuite.AdminEmail
                            }
                            elseif ($part -notlike "*@*.*") {
                                $part = "$($part)@$($Script:PSGSuite.Domain)"
                            }
                        }
                        Write-Verbose "Getting Teacher '$part' for Course '$CourseId'"
                        $request = $service.Courses.Teachers.Get($CourseId,$part)
                        $request.Fields = "$($Fields -join ",")"
                        $request.Execute()
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            List {
                foreach ($Ro in $Role) {
                    try {
                        Write-Verbose "Getting List of $($Ro)s for Course '$CourseId'"
                        $request = switch ($Ro) {
                            Teacher {
                                $service.Courses.Teachers.List($CourseId)
                            }
                            Student {
                                $service.Courses.Students.List($CourseId)
                            }
                        }
                        $request.Fields = "$($Fields -join ",")"
                        [int]$retrieved = 0
                        [int]$i = 1
                        do {
                            $result = $request.Execute()
                            switch ($Ro) {
                                Teacher {
                                    if ($null -ne $result.Teachers) {
                                        $result.Teachers
                                    }
                                    [int]$retrieved = ($i + $result.Teachers.Count) - 1
                                    [int]$i = $i + $result.Teachers.Count
                                }
                                Student {
                                    if ($null -ne $result.Students) {
                                        $result.Students
                                    }
                                    [int]$retrieved = ($i + $result.Students.Count) - 1
                                    [int]$i = $i + $result.Students.Count
                                }
                            }
                            $request.PageToken = $result.NextPageToken
                            Write-Verbose "Retrieved $retrieved $($Ro)s..."
                        }
                        until (!$result.NextPageToken)
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCourseParticipant'
function Get-GSStudentGuardian {
    <#
    .SYNOPSIS
    Gets a guardian or list of guardians for a student.
 
    .DESCRIPTION
    Gets a guardian or list of guardians for a student.
 
    .PARAMETER StudentId
    The identifier of the student to get guardian info for. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
    * the string literal "-", indicating that results should be returned for all students that the requesting user is permitted to view guardians for. [Default]
        * **This is only allowed when excluding the `GuardianId` parameter to perform a List request!**
 
    .PARAMETER GuardianId
    The id field from a Guardian.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Get-GSStudentGuardian
 
    Gets the list of guardians for all students.
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.Guardian')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Student')]
        [String[]]
        $StudentId = "-",
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Guardian')]
        [String[]]
        $GuardianId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.guardianlinks.students'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($stuId in $StudentId) {
            try {
                if ($stuId -ne '-') {
                    if ( -not ($stuId -as [decimal])) {
                        if ($stuId -ceq 'me') {
                            $stuId = $Script:PSGSuite.AdminEmail
                        }
                        elseif ($stuId -notlike "*@*.*") {
                            $stuId = "$($stuId)@$($Script:PSGSuite.Domain)"
                        }
                    }
                }
                elseif ($PSBoundParameters.Keys -contains 'GuardianId') {
                    Write-Error "You must specify a valid StudentId when including a GuardianId! Current value '$stuId'"
                }
                if ($PSBoundParameters.Keys -contains 'GuardianId') {
                    foreach ($guard in $GuardianId) {
                        try {
                            Write-Verbose "Getting Guardian '$guard' for Student '$stuId'"
                            $request = $service.UserProfiles.Guardians.Get($stuId,$guard)
                            $request.Execute()
                        }
                        catch {
                            if ($ErrorActionPreference -eq 'Stop') {
                                $PSCmdlet.ThrowTerminatingError($_)
                            }
                            else {
                                Write-Error $_
                            }
                        }
                    }
                }
                else {
                    try {
                        if ($stuId -eq '-') {
                            Write-Verbose "Listing all Guardians"
                        }
                        else {
                            Write-Verbose "Listing Guardians for Student '$stuId'"
                        }
                        $request = $service.UserProfiles.Guardians.List($stuId)
                        [int]$i = 1
                        do {
                            $result = $request.Execute()
                            if ($null -ne $result.Guardians) {
                                $result.Guardians
                            }
                            $request.PageToken = $result.NextPageToken
                            [int]$retrieved = ($i + $result.Guardians.Count) - 1
                            Write-Verbose "Retrieved $retrieved Guardians..."
                            [int]$i = $i + $result.Guardians.Count
                        }
                        until (!$result.NextPageToken)
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSStudentGuardian'
function Get-GSStudentGuardianInvitation {
    <#
    .SYNOPSIS
    Gets a guardian invitation or list of guardian invitations.
 
    .DESCRIPTION
    Gets a guardian invitation or list of guardian invitations.
 
    .PARAMETER InvitationId
    The id field of the GuardianInvitation being requested.
 
    .PARAMETER StudentId
    The identifier of the student whose guardian invitation is being requested. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
    * the string literal "-", indicating that results should be returned for all students that the requesting user is permitted to view guardian invitations. [Default]
        * **This is only allowed when excluding the `InvitationId` parameter to perform a List request!**
 
    .PARAMETER GuardianEmail
    If specified, only results with the specified GuardianEmail will be returned.
 
    .PARAMETER States
    If specified, only results with the specified state values will be returned. Otherwise, results with a state of PENDING will be returned.
 
    The State can be one of the following:
 
    * PENDING
    * COMPLETE
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Get-GSStudentGuardianInvitation -StudentId aristotle@athens.edu
 
    Gets the list of guardian invitations for this student.
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.GuardianInvitation')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,ParameterSetName = "Get")]
        [Alias('Id')]
        [String[]]
        $InvitationId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Student')]
        [String[]]
        $StudentId = "-",
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true,ParameterSetName = "List")]
        [Alias('Guardian')]
        [String]
        $GuardianEmail,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Google.Apis.Classroom.v1.UserProfilesResource+GuardianInvitationsResource+ListRequest+StatesEnum[]]
        $States,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.guardianlinks.students'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($stuId in $StudentId) {
            try {
                if ($stuId -ne '-') {
                    if ( -not ($stuId -as [decimal])) {
                        if ($stuId -ceq 'me') {
                            $stuId = $Script:PSGSuite.AdminEmail
                        }
                        elseif ($stuId -notlike "*@*.*") {
                            $stuId = "$($stuId)@$($Script:PSGSuite.Domain)"
                        }
                    }
                }
                elseif ($PSCmdlet.ParameterSetName -eq 'Get') {
                    Write-Error "You must specify a valid StudentId when using InvitationId! Current value '$stuId'"
                }
                switch ($PSCmdlet.ParameterSetName) {
                    Get {
                        foreach ($invId in $InvitationId) {
                            try {
                                Write-Verbose "Getting Guardian Invitation '$invId' for Student '$stuId'"
                                $request = $service.UserProfiles.GuardianInvitations.Get($stuId,$invId)
                                $request.Execute()
                            }
                            catch {
                                if ($ErrorActionPreference -eq 'Stop') {
                                    $PSCmdlet.ThrowTerminatingError($_)
                                }
                                else {
                                    Write-Error $_
                                }
                            }
                        }
                    }
                    List {
                        try {
                            if ($stuId -eq '-') {
                                Write-Verbose "Listing all Guardian Invitations"
                            }
                            else {
                                Write-Verbose "Listing Guardian Invitations for Student '$stuId'"
                            }
                            $request = $service.UserProfiles.GuardianInvitations.List($stuId)
                            foreach ($s in $States) {
                                $request.States += $s
                            }
                            if ($PSBoundParameters.Keys -contains 'GuardianEmail') {
                                $request.InvitedEmailAddress = $GuardianEmail
                            }
                            [int]$i = 1
                            do {
                                $result = $request.Execute()
                                if ($null -ne $result.GuardianInvitations) {
                                    $result.GuardianInvitations
                                }
                                $request.PageToken = $result.NextPageToken
                                [int]$retrieved = ($i + $result.GuardianInvitations.Count) - 1
                                Write-Verbose "Retrieved $retrieved Guardian Invitations..."
                                [int]$i = $i + $result.GuardianInvitations.Count
                            }
                            until (!$result.NextPageToken)
                        }
                        catch {
                            if ($ErrorActionPreference -eq 'Stop') {
                                $PSCmdlet.ThrowTerminatingError($_)
                            }
                            else {
                                Write-Error $_
                            }
                        }
                    }
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSStudentGuardianInvitation'
function New-GSCourse {
    <#
    .SYNOPSIS
    Creates a course.
 
    .DESCRIPTION
    Creates a course.
 
    .PARAMETER Name
    Name of the course. For example, "10th Grade Biology". The name is required. It must be between 1 and 750 characters and a valid UTF-8 string.
 
    .PARAMETER OwnerId
    The identifier of the owner of a course.
 
    When specified as a parameter of a create course request, this field is required. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER Id
    Identifier for this course assigned by Classroom.
 
    When creating a course, you may optionally set this identifier to an alias string in the request to create a corresponding alias. The id is still assigned by Classroom and cannot be updated after the course is created.
 
    .PARAMETER Section
    Section of the course. For example, "Period 2". If set, this field must be a valid UTF-8 string and no longer than 2800 characters.
 
    .PARAMETER DescriptionHeading
    Optional heading for the description. For example, "Welcome to 10th Grade Biology." If set, this field must be a valid UTF-8 string and no longer than 3600 characters.
 
    .PARAMETER Description
    Optional description. For example, "We'll be learning about the structure of living creatures from a combination of textbooks, guest lectures, and lab work. Expect to be excited!" If set, this field must be a valid UTF-8 string and no longer than 30,000 characters.
 
    .PARAMETER Room
    Optional room location. For example, "301". If set, this field must be a valid UTF-8 string and no longer than 650 characters.
 
    .PARAMETER CourseState
    State of the course. If unspecified, the default state is PROVISIONED
 
    Available values are:
    * ACTIVE - The course is active.
    * ARCHIVED - The course has been archived. You cannot modify it except to change it to a different state.
    * PROVISIONED - The course has been created, but not yet activated. It is accessible by the primary teacher and domain administrators, who may modify it or change it to the ACTIVE or DECLINED states. A course may only be changed to PROVISIONED if it is in the DECLINED state.
    * DECLINED - The course has been created, but declined. It is accessible by the course owner and domain administrators, though it will not be displayed in the web UI. You cannot modify the course except to change it to the PROVISIONED state. A course may only be changed to DECLINED if it is in the PROVISIONED state.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    New-GSCourse -Name "The Rebublic" -OwnerId plato@athens.edu -Id the-republic-s01 -Section s01 -DescriptionHeading "The definition of justice, the order and character of the just city-state and the just man" -Room academy-01
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.Course')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [ValidateLength(1,750)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [Alias('Teacher')]
        [String]
        $OwnerId,
        [parameter(Mandatory = $false)]
        [Alias('Alias')]
        [String]
        $Id,
        [parameter(Mandatory = $false)]
        [ValidateLength(1,2800)]
        [String]
        $Section,
        [parameter(Mandatory = $false)]
        [ValidateLength(1,3600)]
        [Alias('Heading')]
        [String]
        $DescriptionHeading,
        [parameter(Mandatory = $false)]
        [ValidateLength(1,30000)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [String]
        $Room,
        [parameter(Mandatory = $false)]
        [Alias('Status')]
        [ValidateSet('PROVISIONED','ACTIVE','ARCHIVED','DECLINED')]
        [String]
        $CourseState,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.courses'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            Write-Verbose "Creating new Course '$Name'"
            $body = New-Object 'Google.Apis.Classroom.v1.Data.Course'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    OwnerId {
                        try {
                            [decimal]$PSBoundParameters[$prop] | Out-Null
                        }
                        catch {
                            if ($PSBoundParameters[$prop] -ceq 'me') {
                                $PSBoundParameters[$prop] = $Script:PSGSuite.AdminEmail
                            }
                            elseif ($PSBoundParameters[$prop] -notlike "*@*.*") {
                                $PSBoundParameters[$prop] = "$($PSBoundParameters[$prop])@$($Script:PSGSuite.Domain)"
                            }
                        }
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            if ($PSBoundParameters.Keys -notcontains 'OwnerId') {
                $body.OwnerId = $Script:PSGSuite.AdminEmail
            }
            $request = $service.Courses.Create($body)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSCourse'
function New-GSCourseAlias {
    <#
    .SYNOPSIS
    Creates a course alias.
 
    .DESCRIPTION
    Creates a course alias.
 
    .PARAMETER Alias
    Alias string
 
    .PARAMETER CourseId
    Identifier of the course to alias. This identifier can be either the Classroom-assigned identifier or an alias.
 
    .PARAMETER Scope
    An alias uniquely identifies a course. It must be unique within one of the following scopes:
 
    * Domain - A domain-scoped alias is visible to all users within the alias creator's domain and can be created only by a domain admin. A domain-scoped alias is often used when a course has an identifier external to Classroom.
    * Project - A project-scoped alias is visible to any request from an application using the Developer Console project ID that created the alias and can be created by any project. A project-scoped alias is often used when an application has alternative identifiers. A random value can also be used to avoid duplicate courses in the event of transmission failures, as retrying a request will return ALREADY_EXISTS if a previous one has succeeded.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    New-GSCourseAlias -Alias "abc123" -CourseId 'architecture-101' -Scope Domain
 
    .EXAMPLE
    New-GSCourseAlias -Alias "d:abc123" -CourseId 'architecture-101'
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.CourseAlias')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $Alias,
        [parameter(Mandatory = $true,Position = 1)]
        [String]
        $CourseId,
        [parameter(Mandatory = $false)]
        [ValidateSet('Domain','Project')]
        [String]
        $Scope = $(if($Alias -match "^p\:"){'Project'}else{'Domain'}),
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $formatted = if ($Alias -match "^(d\:|p\:)") {
            $Alias
            $Scope = if ($Alias -match "^d\:") {
                'Domain'
            }
            else {
                'Project'
            }
        }
        else {
            switch ($Scope) {
                Domain {
                    'd:' + ($Alias -replace "^(d\:|p\:)","")
                }
                Project {
                    'p:' + ($Alias -replace "^(d\:|p\:)","")
                }
            }

        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.courses'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            Write-Verbose "Creating new Alias '$Alias' for Course '$CourseId' at '$Scope' scope"
            $body = New-Object 'Google.Apis.Classroom.v1.Data.CourseAlias' -Property @{
                Alias = $formatted
            }
            $request = $service.Courses.Aliases.Create($body,$CourseId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSCourseAlias'
function New-GSCourseInvitation {
    <#
    .SYNOPSIS
    Creates a course invitation.
 
    .DESCRIPTION
    Creates a course invitation.
 
    .PARAMETER CourseId
    Identifier of the course to invite the user to. This identifier can be either the Classroom-assigned identifier or an alias.
 
    .PARAMETER UserId
    Identifier of the user to invite. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER Role
    Role to invite the user to have from the following:
 
    * STUDENT
    * TEACHER
    * OWNER
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    New-GSCourseInvitation -CourseId philosophy-101 -UserId aristotle@athens.edu -Role TEACHER
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.Invitation')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [String]
        $CourseId,
        [parameter(Mandatory = $false,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('PrimaryEmail','Email','Mail')]
        [String[]]
        $UserId,
        [parameter(Mandatory = $false)]
        [ValidateSet('STUDENT','TEACHER','OWNER')]
        [String]
        $Role = 'STUDENT',
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.rosters'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($U in $UserId) {
            try {
                if ( -not ($U -as [decimal])) {
                    if ($U -ceq 'me') {
                        $U = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($U -notlike "*@*.*") {
                        $U = "$($U)@$($Script:PSGSuite.Domain)"
                    }
                }
                Write-Verbose "Inviting User '$U' to Course '$CourseId' for Role '$Role'"
                $body = New-Object 'Google.Apis.Classroom.v1.Data.Invitation'
                foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                    $body.$prop = $PSBoundParameters[$prop]
                }
                if ($PSBoundParameters.Keys -notcontains 'Role') {
                    $body.Role = $Role
                }
                $request = $service.Invitations.Create($body)
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSCourseInvitation'
function New-GSStudentGuardianInvitation {
    <#
    .SYNOPSIS
    Creates a guardian invitation, and sends an email to the guardian asking them to confirm that they are the student's guardian.
 
    .DESCRIPTION
    Creates a guardian invitation, and sends an email to the guardian asking them to confirm that they are the student's guardian.
 
    .PARAMETER StudentId
    Identifier of the user to invite. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
 
    .PARAMETER GuardianEmail
    The email address of the guardian to invite.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    New-GSStudentGuardianInvitation -StudentId aristotle@athens.edu -GuardianEmail zeus@olympus.io
 
    .EXAMPLE
    Import-Csv .\Student_Guardian_List.csv | New-GSStudentGuardianInvitation
 
    Process a CSV with two columns containing headers "Student" and "Guardian" and send the invites accordingly, i.e.
 
    | StudentId | GuardianEmail |
    |:--------------------:|:---------------:|
    | aristotle@athens.edu | zeus@olympus.io |
    | plato@athens.edu | hera@olympus.io |
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.GuardianInvitation')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Student')]
        [String]
        $StudentId,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Guardian')]
        [String]
        $GuardianEmail,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.guardianlinks.students'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ( -not ($StudentId -as [decimal])) {
                if ($StudentId -ceq 'me') {
                    $StudentId = $Script:PSGSuite.AdminEmail
                }
                elseif ($StudentId -notlike "*@*.*") {
                    $StudentId = "$($StudentId)@$($Script:PSGSuite.Domain)"
                }
            }
            $body = New-Object 'Google.Apis.Classroom.v1.Data.GuardianInvitation' -Property @{
                StudentId = $StudentId
                InvitedEmailAddress = $GuardianEmail
            }
            Write-Verbose "Inviting Guardian '$GuardianEmail' for Student '$StudentId'"
            $request = $service.UserProfiles.GuardianInvitations.Create($body,$StudentId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSStudentGuardianInvitation'
function Remove-GSCourse {
    <#
    .SYNOPSIS
    Removes an existing course.
 
    .DESCRIPTION
    Removes an existing course.
 
    .PARAMETER Id
    Identifier for this course assigned by Classroom.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Remove-GSCourse -Id the-republic-s01 -Confirm:$false
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [Alias('Alias')]
        [String]
        $Id,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.courses'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($PSCmdlet.ShouldProcess("Removing Course '$Id'")) {
                Write-Verbose "Removing Course '$Id'"
                $request = $service.Courses.Delete($Id)
                $request.Execute()
                Write-Verbose "Course '$Id' has been successfully removed"
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSCourse'
function Remove-GSCourseAlias {
    <#
    .SYNOPSIS
    Removes a course alias.
 
    .DESCRIPTION
    Removes a course alias.
 
    .PARAMETER Alias
    Alias string
 
    .PARAMETER CourseId
    Identifier of the course to alias. This identifier can be either the Classroom-assigned identifier or an alias.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Remove-GSCourseAlias -Alias "d:abc123" -CourseId 'architecture-101'
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $Alias,
        [parameter(Mandatory = $true,Position = 1)]
        [String]
        $CourseId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.courses'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($PSCmdlet.ShouldProcess("Removing Alias '$Alias' for Course '$CourseId'")) {
                Write-Verbose "Removing Alias '$Alias' for Course '$CourseId'"
                $request = $service.Courses.Aliases.Delete($CourseId,$Alias)
                $request.Execute()
                Write-Verbose "Alias '$Alias' for Course '$CourseId' has been successfully removed"
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSCourseAlias'
function Remove-GSCourseInvitation {
    <#
    .SYNOPSIS
    Deletes an invitation.
 
    .DESCRIPTION
    Deletes an invitation.
 
    .PARAMETER Id
    Identifier of the invitation to delete.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Remove-GSCourseInvitation -Id $inviteId -Confirm:$false
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [String[]]
        $Id,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.rosters'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($I in $Id) {
            try {
                if ($PSCmdlet.ShouldProcess("Removing Invitation '$I'")) {
                    Write-Verbose "Removing Invitation '$I'"
                    $request = $service.Invitations.Delete($I)
                    $request.Execute()
                    Write-Verbose "Invitation '$I' has been successfully removed"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSCourseInvitation'
function Remove-GSCourseParticipant {
    <#
    .SYNOPSIS
    Removes students and/or teachers from a course
 
    .DESCRIPTION
    Removes students and/or teachers from a course
 
    .PARAMETER CourseId
    Identifier of the course to remove participants from. This identifier can be either the Classroom-assigned identifier or an alias.
 
    .PARAMETER Student
    Identifier of the user.
 
    This identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER Teacher
    Identifier of the user.
 
    This identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Remove-GSCourseParticipant -CourseId 'architecture-101' -Student plato@athens.edu,aristotle@athens.edu -Teacher zeus@athens.edu
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $CourseId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('PrimaryEmail','Email','Mail')]
        [String[]]
        $Student,
        [parameter(Mandatory = $false)]
        [String[]]
        $Teacher,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.rosters'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($part in $Student) {
            try {
                if ( -not ($part -as [decimal])) {
                    if ($part -ceq 'me') {
                        $part = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($part -notlike "*@*.*") {
                        $part = "$($part)@$($Script:PSGSuite.Domain)"
                    }
                }
                if ($PSCmdlet.ShouldProcess("Removing Student '$part' from Course '$CourseId'")) {
                    Write-Verbose "Removing Student '$part' from Course '$CourseId'"
                    $request = $service.Courses.Students.Delete($CourseId,$part)
                    $request.Execute()
                    Write-Verbose "Student '$part' has successfully been removed from Course '$CourseId'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
        foreach ($part in $Teacher) {
            try {
                try {
                    [decimal]$part | Out-Null
                }
                catch {
                    if ($part -ceq 'me') {
                        $part = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($part -notlike "*@*.*") {
                        $part = "$($part)@$($Script:PSGSuite.Domain)"
                    }
                }
                if ($PSCmdlet.ShouldProcess("Removing Teacher '$part' from Course '$CourseId'")) {
                    Write-Verbose "Removing Teacher '$part' from Course '$CourseId'"
                    $request = $service.Courses.Teachers.Delete($CourseId,$part)
                    $request.Execute()
                    Write-Verbose "Teacher '$part' has successfully been removed from Course '$CourseId'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSCourseParticipant'
function Remove-GSStudentGuardian {
    <#
    .SYNOPSIS
    Removes a guardian.
 
    .DESCRIPTION
    Removes a guardian.
 
    .PARAMETER StudentId
    The identifier of the student to get guardian info for. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER GuardianId
    The id field from a Guardian.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Remove-GSStudentGuardian -StudentId aristotle@athens.edu -GuardianId $guardianId
 
    Removes the guardian for artistotle@athens.edu.
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Student')]
        [String]
        $StudentId,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Guardian')]
        [String]
        $GuardianId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.guardianlinks.students'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ( -not ($StudentId -as [decimal])) {
                if ($StudentId -ceq 'me') {
                    $StudentId = $Script:PSGSuite.AdminEmail
                }
                elseif ($StudentId -notlike "*@*.*") {
                    $StudentId = "$($StudentId)@$($Script:PSGSuite.Domain)"
                }
            }
            if ($PSCmdlet.ShouldProcess("Removing Guardian '$GuardianId' from Student '$StudentId'")) {
                Write-Verbose "Removing Guardian '$GuardianId' from Student '$StudentId'"
                $request = $service.UserProfiles.Guardians.Delete($StudentId,$GuardianId)
                $request.Execute()
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSStudentGuardian'
function Revoke-GSStudentGuardianInvitation {
    <#
    .SYNOPSIS
    Revokes a student guardian invitation.
 
    .DESCRIPTION
    Revokes a student guardian invitation.
 
    This method returns the following error codes:
 
    * PERMISSION_DENIED if the current user does not have permission to manage guardians, if guardians are not enabled for the domain in question or for other access errors.
    * FAILED_PRECONDITION if the guardian link is not in the PENDING state.
    * INVALID_ARGUMENT if the format of the student ID provided cannot be recognized (it is not an email address, nor a user_id from this API), or if the passed GuardianInvitation has a state other than COMPLETE, or if it modifies fields other than state.
    * NOT_FOUND if the student ID provided is a valid student ID, but Classroom has no record of that student, or if the id field does not refer to a guardian invitation known to Classroom.
 
    .PARAMETER StudentId
    The ID of the student whose guardian invitation is to be revoked. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
 
    .PARAMETER InvitationId
    The id field of the GuardianInvitation to be revoked.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Revoke-GSStudentGuardianInvitation -StudentId aristotle@athens.edu -InvitationId $invitationId
 
    .EXAMPLE
    Import-Csv .\Student_Guardian_List_To_Revoke.csv | Revoke-GSStudentGuardianInvitation
 
    Process a CSV with two columns containing headers "Student" and "Guardian" and revokes the invites accordingly, i.e.
 
    | StudentId | InvitationId |
    |:--------------------:|:------------------:|
    | aristotle@athens.edu | 198okj4k9827872177 |
    | plato@athens.edu | 09120uuip21ru0ff0u |
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.GuardianInvitation')]
    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Student')]
        [String]
        $StudentId,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Invitation','InviteId','Invite')]
        [String[]]
        $InvitationId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.guardianlinks.students'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        if ( -not ($StudentId -as [decimal])) {
            if ($StudentId -ceq 'me') {
                $StudentId = $Script:PSGSuite.AdminEmail
            }
            elseif ($StudentId -notlike "*@*.*") {
                $StudentId = "$($StudentId)@$($Script:PSGSuite.Domain)"
            }
        }
        foreach ($invId in $InvitationId) {
            try {
                if ($PSCmdlet.ShouldProcess("Revoking Guardian Invitation '$invId' for Student '$StudentId'")) {
                    Write-Verbose "Revoking Guardian Invitation '$invId' for Student '$StudentId'"
                    $body = New-Object 'Google.Apis.Classroom.v1.Data.GuardianInvitation' -Property @{
                        State = "COMPLETE"
                    }
                    $request = $service.UserProfiles.GuardianInvitations.Patch($body,$StudentId,$invId)
                    $request.UpdateMask = "state"
                    $request.Execute()
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Revoke-GSStudentGuardianInvitation'
function Update-GSCourse {
    <#
    .SYNOPSIS
    Updates an existing course.
 
    .DESCRIPTION
    Updates an existing course.
 
    .PARAMETER Id
    Identifier for this course assigned by Classroom.
 
    .PARAMETER Name
    Name of the course. For example, "10th Grade Biology". The name is required. It must be between 1 and 750 characters and a valid UTF-8 string.
 
    .PARAMETER OwnerId
    The identifier of the owner of a course.
 
    When specified as a parameter of a create course request, this field is required. The identifier can be one of the following:
 
    * the numeric identifier for the user
    * the email address of the user
    * the string literal "me", indicating the requesting user
 
    .PARAMETER Section
    Section of the course. For example, "Period 2". If set, this field must be a valid UTF-8 string and no longer than 2800 characters.
 
    .PARAMETER DescriptionHeading
    Optional heading for the description. For example, "Welcome to 10th Grade Biology." If set, this field must be a valid UTF-8 string and no longer than 3600 characters.
 
    .PARAMETER Description
    Optional description. For example, "We'll be learning about the structure of living creatures from a combination of textbooks, guest lectures, and lab work. Expect to be excited!" If set, this field must be a valid UTF-8 string and no longer than 30,000 characters.
 
    .PARAMETER Room
    Optional room location. For example, "301". If set, this field must be a valid UTF-8 string and no longer than 650 characters.
 
    .PARAMETER CourseState
    State of the course. If unspecified, the default state is PROVISIONED
 
    Available values are:
    * ACTIVE - The course is active.
    * ARCHIVED - The course has been archived. You cannot modify it except to change it to a different state.
    * PROVISIONED - The course has been created, but not yet activated. It is accessible by the primary teacher and domain administrators, who may modify it or change it to the ACTIVE or DECLINED states. A course may only be changed to PROVISIONED if it is in the DECLINED state.
    * DECLINED - The course has been created, but declined. It is accessible by the course owner and domain administrators, though it will not be displayed in the web UI. You cannot modify the course except to change it to the PROVISIONED state. A course may only be changed to DECLINED if it is in the PROVISIONED state.
 
    .PARAMETER User
    The user to authenticate the request as
 
    .EXAMPLE
    Update-GSCourse -Id the-republic-s01 -Name "The Rebublic 101"
    #>

    [OutputType('Google.Apis.Classroom.v1.Data.Course')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [Alias('Alias')]
        [String]
        $Id,
        [parameter(Mandatory = $false)]
        [ValidateLength(1,750)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [Alias('Teacher')]
        [String]
        $OwnerId,
        [parameter(Mandatory = $false)]
        [ValidateLength(1,2800)]
        [String]
        $Section,
        [parameter(Mandatory = $false)]
        [ValidateLength(1,3600)]
        [Alias('Heading')]
        [String]
        $DescriptionHeading,
        [parameter(Mandatory = $false)]
        [ValidateLength(1,30000)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [String]
        $Room,
        [parameter(Mandatory = $false)]
        [Alias('Status')]
        [ValidateSet('PROVISIONED','ACTIVE','ARCHIVED','DECLINED')]
        [String]
        $CourseState,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $UpdateMask = @()
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/classroom.courses'
            ServiceType = 'Google.Apis.Classroom.v1.ClassroomService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            Write-Verbose "Updating Course ID '$Id'"
            $body = New-Object 'Google.Apis.Classroom.v1.Data.Course'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    Id {}
                    OwnerId {
                        $UpdateMask += $($prop.Substring(0,1).ToLower() + $prop.Substring(1))
                        try {
                            [decimal]$PSBoundParameters[$prop] | Out-Null
                        }
                        catch {
                            if ($PSBoundParameters[$prop] -ceq 'me') {
                                $PSBoundParameters[$prop] = $Script:PSGSuite.AdminEmail
                            }
                            elseif ($PSBoundParameters[$prop] -notlike "*@*.*") {
                                $PSBoundParameters[$prop] = "$($PSBoundParameters[$prop])@$($Script:PSGSuite.Domain)"
                            }
                        }
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                    Default {
                        $UpdateMask += $($prop.Substring(0,1).ToLower() + $prop.Substring(1))
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            $request = $service.Courses.Patch($body,$Id)
            $request.UpdateMask = $($UpdateMask -join ",")
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSCourse'
function Export-PSGSuiteConfig {
    <#
    .SYNOPSIS
    Allows you to export an unecrypted PSGSuite config in a portable JSON string format. Useful for moving a config to a new machine or storing the full as an encrypted string in your CI/CD / Automation tools.
 
    .DESCRIPTION
    Allows you to export an unecrypted PSGSuite config in a portable JSON string format. Useful for moving a config to a new machine or storing the full as an encrypted string in your CI/CD / Automation tools.
 
    .PARAMETER Path
    The path you would like to save the JSON file to. Defaults to a named path in the current directory.
 
    .PARAMETER ConfigName
    The config that you would like to export. Defaults to the currently loaded config.
 
    .EXAMPLE
    Export-PSGSuiteConfig -ConfigName Personal -Path ".\PSGSuite_personal_config.json"
 
    Exports the config named 'Personal' to the path specified.
    #>

    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $false,Position = 0)]
        [Alias('OutPath','OutFile','JsonPath')]
        [String]
        $Path,
        [parameter(Mandatory = $false)]
        [String]
        $ConfigName = $script:PSGSuite.ConfigName
    )
    Begin {
        $baseConf = if ($PSBoundParameters.Keys -contains 'ConfigName'){
            Get-PSGSuiteConfig -ConfigName $ConfigName -NoImport -PassThru -Verbose:$false
        }
        else {
            Show-PSGSuiteConfig -Verbose:$false
        }
        if ($PSBoundParameters.Keys -notcontains 'Path') {
            $Path = (Join-Path $PWD.Path "PSGSuite_$($baseConf.AdminEmail)_$($baseConf.ConfigName).json")
        }
    }
    Process {
        try {
            Write-Verbose "Exporting config '$ConfigName' to path: $Path"
            $baseConf | Select-Object ConfigName,P12Key,ClientSecrets,AppEmail,AdminEmail,CustomerId,Domain,Preference | ConvertTo-Json -Depth 5 -Compress -Verbose:$false | Set-Content -Path $Path -Verbose:$false
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

Export-ModuleMember -Function 'Export-PSGSuiteConfig'
function Get-GSChatConfig {
    <#
    .SYNOPSIS
    Returns the specified Chat space and webhook dictionaries from the PSGSuite config to use with Send-GSChatMessage
 
    .DESCRIPTION
    Returns the specified Chat space and webhook dictionaries from the PSGSuite config to use with Send-GSChatMessage
 
    .PARAMETER WebhookName
    The key that the Webhook Url is stored as in the Config. If left blank, returns the full Chat configuration from the Config
 
    .PARAMETER SpaceName
    The key that the Space ID is stored as in the Config. If left blank, returns the full Chat configuration from the Config
 
    .PARAMETER ConfigName
    The name of the Config to return the Chat config items from
 
    .EXAMPLE
    Send-GSChatMessage -Text "Testing webhook" -Webhook (Get-GSChatConfig MyRoom)
     
    Sends a Chat message with text to the Webhook Url named 'MyRoom' found in the config
    #>

    [CmdletBinding(DefaultParameterSetName = "Webhooks")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ParameterSetName = "Webhooks")]
        [String[]]
        $WebhookName,
        [parameter(Mandatory = $false,Position = 0,ParameterSetName = "Spaces")]
        [String[]]
        $SpaceName,
        [parameter(Mandatory = $false,Position = 1)]
        [String[]]
        $ConfigName
    )
    if ($PSBoundParameters.Keys -contains 'ConfigName') {
        $currentConfig = Get-PSGSuiteConfig -ConfigName $ConfigName -PassThru -NoImport
    }
    else {
        $currentConfig = Get-PSGSuiteConfig -PassThru
    }
    switch ($PSCmdlet.ParameterSetName) {
        Webhooks {
            if ($PSBoundParameters.Keys -contains 'WebhookName') {
                foreach ($hook in $WebhookName) {
                    Write-Verbose "Getting webhook for '$hook' from ConfigName '$($currentConfig.ConfigName)'"
                    if ($found = $currentConfig.Chat['Webhooks'][$hook]) {
                        $found
                    }
                    else {
                        Write-Error "$hook was not found in the Webhook dictionary stored in ConfigName '$($currentConfig.ConfigName)'!"
                    }
                }
            }
            else {
                Write-Verbose "Getting full Chat config from ConfigName '$($currentConfig.ConfigName)'"
                $currentConfig.Chat
            }
        }
        Spaces {
            foreach ($hook in $SpaceName) {
                Write-Verbose "Getting space Id for '$hook' from ConfigName '$($currentConfig.ConfigName)'"
                if ($found = $currentConfig.Chat['Spaces'][$hook]) {
                    $found
                }
                else {
                    Write-Error "$hook was not found in the Spaces dictionary stored in ConfigName '$($currentConfig.ConfigName)'!"
                }
            }
        }
    }
}
Export-ModuleMember -Function 'Get-GSChatConfig'
function Get-PSGSuiteConfig {
    <#
    .SYNOPSIS
    Loads the specified PSGSuite config
 
    .DESCRIPTION
    Loads the specified PSGSuite config
 
    .PARAMETER ConfigName
    The config name to load
 
    .PARAMETER Path
    The path of the config to load if non-default.
 
    This can be used to load either a legacy XML config from an older version of PSGSuite or a specific .PSD1 config created with version 2.0.0 or greater
 
    .PARAMETER Scope
    The config scope to load
 
    .PARAMETER PassThru
    If specified, returns the config after loading it
 
    .PARAMETER NoImport
    If $true, just returns the specified config but does not impart it in the current session.
 
    .EXAMPLE
    Get-PSGSuiteConfig personalDomain -PassThru
 
    This will load the config named "personalDomain" and return it as a PSObject.
    #>

    [cmdletbinding(DefaultParameterSetName = "ConfigurationModule")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ParameterSetName = "ConfigurationModule")]
        [String]
        $ConfigName,
        [Parameter(Mandatory = $false,ParameterSetName = "Path")]
        [ValidateScript( {Test-Path $_})]
        [String]
        $Path,
        [Parameter(Mandatory = $false,Position = 1)]
        [ValidateSet("User", "Machine", "Enterprise", $null)]
        [string]
        $Scope = $Script:ConfigScope,
        [Parameter(Mandatory = $false)]
        [Switch]
        $PassThru,
        [Parameter(Mandatory = $false)]
        [Switch]
        $NoImport
    )
    Process {
        $script:ConfigScope = $Scope
        switch ($PSCmdlet.ParameterSetName) {
            ConfigurationModule {
                $fullConf = Import-SpecificConfiguration -CompanyName 'SCRT HQ' -Name 'PSGSuite' -Scope $Script:ConfigScope
                if (!$ConfigName) {
                    $choice = $fullConf["DefaultConfig"]
                    Write-Verbose "Importing default config: $choice"
                }
                else {
                    $choice = $ConfigName
                    Write-Verbose "Importing config: $choice"
                }
                $encConf = [PSCustomObject]($fullConf[$choice])
            }
            Path {
                $encConf = switch ((Get-Item -Path $Path).Extension) {
                    '.xml' {
                        Import-Clixml -Path $Path
                        $choice = "LegacyXML"
                    }
                    '.psd1' {
                        Import-SpecificConfiguration -Path $Path
                        $choice = "CustomConfigurationFile"
                    }
                }
            }
        }
        $decryptParams = @{
            ConfigName = $choice
        }
        if ($Path) {
            $decryptParams['ConfigPath'] = $Path
        }
        $decryptedConfig = $encConf | Get-GSDecryptedConfig @decryptParams
        Write-Verbose "Retrieved configuration '$choice'"
        if (!$NoImport) {
            $script:PSGSuite = $decryptedConfig
        }
        if ($PassThru) {
            $decryptedConfig
        }
    }
}

Export-ModuleMember -Function 'Get-PSGSuiteConfig'
function Import-PSGSuiteConfig {
    <#
    .SYNOPSIS
    Allows you to import an unecrypted PSGSuite config from a portable JSON string format, typically created with Export-PSGSuiteConfig. Useful for moving a config to a new machine or storing the full as an encrypted string in your CI/CD / Automation tools.
 
    .DESCRIPTION
    Allows you to import an unecrypted PSGSuite config from a portable JSON string format, typically created with Export-PSGSuiteConfig. Useful for moving a config to a new machine or storing the full as an encrypted string in your CI/CD / Automation tools.
 
    .PARAMETER Json
    The Json string to import.
 
    .PARAMETER Path
    The path of the Json file you would like import.
 
    .PARAMETER Temporary
    If $true, the imported config is not stored in the config file and the imported config persists only for the current session.
 
    .PARAMETER PassThru
    If $true, outputs the resulting config object to the pipeline.
 
    .EXAMPLE
    Import-Module PSGSuite -MinimumVersion 2.22.0
    Import-PSGSuiteConfig -Json '$(PSGSuiteConfigJson)' -Temporary
 
    Azure Pipelines inline script task that uses a Secure Variable named 'PSGSuiteConfigJson' with the Config JSON string stored in it, removing the need to include credential or key files anywhere.
    #>

    [CmdletBinding(DefaultParameterSetName = "Json")]
    Param (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ParameterSetName = "Json")]
        [Alias('J')]
        [String]
        $Json,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Path")]
        [Alias('P')]
        [String]
        $Path,
        [parameter(Mandatory = $false)]
        [Alias('Temp','T')]
        [Switch]
        $Temporary,
        [parameter(Mandatory = $false)]
        [Switch]
        $PassThru
    )
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Path {
                    Write-Verbose "Importing config from path: $Path"
                    $script:PSGSuite = (ConvertFrom-Json (Get-Content $Path -Raw))
                }
                Json {
                    Write-Verbose "Importing config from Json string"
                    $script:PSGSuite = (ConvertFrom-Json $Json)
                }
            }
            if (-not $Temporary) {
                Write-Verbose "Saving imported config"
                $script:PSGSuite | Set-PSGSuiteConfig
            }
            if ($PassThru) {
                return $script:PSGSuite
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

Export-ModuleMember -Function 'Import-PSGSuiteConfig'
function Set-PSGSuiteConfig {
    <#
    .SYNOPSIS
    Creates or updates a config
 
    .DESCRIPTION
    Creates or updates a config
 
    .PARAMETER ConfigName
    The friendly name for the config you are creating or updating
 
    .PARAMETER P12KeyPath
    The path to the P12 Key file downloaded from the Google Developer's Console. If both P12KeyPath and ClientSecretsPath are specified, P12KeyPath takes precedence.
 
    .PARAMETER P12Key
    The P12Key in byte array format. If the actual P12Key is present on the config, the P12KeyPath is not needed. The config will auto-update with this value after running any command, if P12KeyPath is filled and this value is not already present.
 
    .PARAMETER P12KeyPassword
    The password for the P12 Key file. If not specified the default of 'notasecret' will be used and this config value will not be set. This is only needed in the case where the P12 file has been manually rexported with a custom password
 
    .PARAMETER ClientSecretsPath
    The path to the Client Secrets JSON file downloaded from the Google Developer's Console. Using the ClientSecrets JSON will prompt the user to complete OAuth2 authentication in their browser on the first run and store the retrieved Refresh and Access tokens in the user's home directory. The config will auto-update with this value after running any command, if ClientSecretsPath is filled and this value is not already present. If JSONServiceAccountKeyPath or P12KeyPath is also specified, ClientSecretsPath will be ignored.
 
    .PARAMETER ClientSecrets
    The string contents of the Client Secrets JSON file downloaded from the Google Developer's Console. Using the ClientSecrets JSON will prompt the user to complete OAuth2 authentication in their browser on the first run and store the retrieved Refresh and Access tokens in the user's home directory. If JSONServiceAccountKeyPath or P12KeyPath is also specified, ClientSecrets will be ignored.
 
    .PARAMETER AppEmail
    The application email from the Google Developer's Console. This typically looks like the following:
 
    myProjectName@myProject.iam.gserviceaccount.com
 
    .PARAMETER AdminEmail
    The email of the Google Admin running the functions. This will typically be your email.
 
    .PARAMETER CustomerID
    The Customer ID for your customer. If unknown, you can retrieve it by running Get-GSUser after creating a base config with at least either the P12KeyPath or ClientSecretsPath, the AppEmail and the AdminEmail.
 
    .PARAMETER Domain
    The domain that you primarily manage for this CustomerID
 
    .PARAMETER Preference
    Some functions allow you to specify whether you are running in the context of the customer or a specific domain in the customer's realm. This allows you to set your preference.
 
    Available values are:
    * CustomerID
    * Domain
 
    .PARAMETER ServiceAccountClientID
    The Service Account's Client ID from the Google Developer's Console. This is optional and is only used as a reference for yourself to prevent needing to check the Developer's Console for the ID when verifying API Client Access.
 
    .PARAMETER Webhook
    Chat Webhooks to add to the config.
 
    .PARAMETER Space
    Chat spaces to add to the config.
 
    .PARAMETER Scope
    The scope at which you would like to set this config.
 
    Available values are:
    * Machine (this would create the config in a location accessible by all users on the machine)
    * Enterprise (this would create the config in the Roaming AppData folder for the user or it's *nix equivalent)
    * User (this would create the config in the Local AppData folder for the user or it's *nix equivalent)
 
    .PARAMETER SetAsDefaultConfig
    If passed, sets the ConfigName as the default config to load on module import
 
    .PARAMETER NoImport
    The default behavior when using Set-PSGSuiteConfig is that the new/updated config is imported as active. If -NoImport is passed, this saves the config but retains the previously loaded config as active.
 
    .EXAMPLE
    Set-PSGSuiteConfig -ConfigName "personal" -P12KeyPath C:\Keys\PersonalKey.p12 -AppEmail "myProjectName@myProject.iam.gserviceaccount.com" -AdminEmail "admin@domain.com" -CustomerID "C83030001" -Domain "domain.com" -Preference CustomerID -ServiceAccountClientID 1175798883298324983498 -SetAsDefaultConfig
 
    This builds a config names "personal" and sets it as the default config
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [ValidateScript( {
                if ($_ -eq "DefaultConfig") {
                    throw "You must specify a ConfigName other than 'DefaultConfig'. That is a reserved value."
                }
                elseif ($_ -notmatch '^[a-zA-Z]+[a-zA-Z0-9]*$') {
                    throw "You must specify a ConfigName that starts with a letter and does not contain any spaces, otherwise the Configuration will break"
                }
                else {
                    $true
                }
            })]
        [string]
        $ConfigName = $Script:ConfigName,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $P12KeyPath,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Byte[]]
        $P12Key,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [SecureString]
        $P12KeyPassword,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $ClientSecretsPath,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $ClientSecrets,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $AppEmail,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $AdminEmail,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $CustomerID,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $Domain,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [ValidateSet("CustomerID","Domain")]
        [string]
        $Preference,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $ServiceAccountClientID,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Hashtable[]]
        $Webhook,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Hashtable[]]
        $Space,
        [parameter(Mandatory = $false)]
        [ValidateSet("User", "Machine", "Enterprise", $null)]
        [string]
        $Scope = $script:ConfigScope,
        [parameter(Mandatory = $false)]
        [switch]
        $SetAsDefaultConfig,
        [parameter(Mandatory = $false)]
        [switch]
        $NoImport
    )
    Process {
        $script:ConfigScope = $Scope
        $configHash = Import-SpecificConfiguration -CompanyName 'SCRT HQ' -Name 'PSGSuite'
        if (!$ConfigName) {
            $ConfigName = if ($configHash["DefaultConfig"]){
                $configHash["DefaultConfig"]
            }
            else {
                "default"
                $configHash["DefaultConfig"] = "default"
            }
        }
        Write-Verbose "Setting config name '$ConfigName'"
        $configParams = @('P12Key','P12KeyPath','P12KeyPassword','ClientSecretsPath','ClientSecrets','AppEmail','AdminEmail','CustomerID','Domain','Preference','ServiceAccountClientID','Webhook','Space')
        if ($SetAsDefaultConfig -or !$configHash["DefaultConfig"]) {
            $configHash["DefaultConfig"] = $ConfigName
        }
        if (!$configHash[$ConfigName]) {
            $configHash.Add($ConfigName,(@{}))
        }
        foreach ($key in ($PSBoundParameters.Keys | Where-Object {$configParams -contains $_})) {
            switch ($key) {
                P12Key {
                    if (-not $_p12Key) {
                        $_p12Key = @()
                    }
                    if ($P12Key.Count -gt 1) {
                        $_p12Key = $P12Key
                    }
                    else {
                        $_p12Key += $P12Key
                    }
                }
                P12KeyPath {
                    if (-not [System.String]::IsNullOrWhiteSpace($PSBoundParameters[$key].Trim())) {
                        $configHash["$ConfigName"][$key] = (Invoke-GSEncrypt $PSBoundParameters[$key])
                        $configHash["$ConfigName"]['P12Key'] = ([System.IO.File]::ReadAllBytes($PSBoundParameters[$key]))
                    }
                }
                P12KeyPassword {
                    $configHash["$ConfigName"][$key] = $PSBoundParameters[$key]
                }
                ClientSecretsPath {
                    if (-not [System.String]::IsNullOrWhiteSpace($PSBoundParameters[$key].Trim())) {
                        $configHash["$ConfigName"][$key] = (Invoke-GSEncrypt $PSBoundParameters[$key])
                        $configHash["$ConfigName"]['ClientSecrets'] = (Invoke-GSEncrypt $(Get-Content $PSBoundParameters[$key] -Raw))
                    }
                }
                Webhook {
                    if ($configHash["$ConfigName"].Keys -notcontains 'Chat') {
                        $configHash["$ConfigName"]['Chat'] = @{
                            Webhooks = @{}
                            Spaces = @{}
                        }
                    }
                    foreach ($cWebhook in $PSBoundParameters[$key]) {
                        foreach ($cWebhookKey in $cWebhook.Keys) {
                            $configHash["$ConfigName"]['Chat']['Webhooks'][$cWebhookKey] = (Invoke-GSEncrypt $cWebhook[$cWebhookKey])
                        }
                    }
                }
                Space {
                    if ($configHash["$ConfigName"].Keys -notcontains 'Chat') {
                        $configHash["$ConfigName"]['Chat'] = @{
                            Webhooks = @{}
                            Spaces = @{}
                        }
                    }
                    $configHash["$ConfigName"]['Chat']['Spaces'] = @{}
                    foreach ($cWebhook in $PSBoundParameters[$key]) {
                        foreach ($cWebhookKey in $cWebhook.Keys) {
                            $configHash["$ConfigName"]['Chat']['Spaces'][$cWebhookKey] = (Invoke-GSEncrypt $cWebhook[$cWebhookKey])
                        }
                    }
                }
                default {
                    $configHash["$ConfigName"][$key] = (Invoke-GSEncrypt $PSBoundParameters[$key])
                }
            }
        }
    }
    End {
        if ($_p12Key) {
            $configHash["$ConfigName"]['P12Key'] = $_p12Key
        }
        $configHash["$ConfigName"]['ConfigPath'] = (Join-Path $(Get-Module PSGSuite | Get-ConfigurationPath -Scope $Script:ConfigScope) "Configuration.psd1")
        $configHash | Export-Configuration -CompanyName 'SCRT HQ' -Name 'PSGSuite' -Scope $script:ConfigScope
        if (!$NoImport) {
            Get-PSGSuiteConfig -ConfigName $ConfigName -Verbose:$false
        }
    }
}

Export-ModuleMember -Function 'Set-PSGSuiteConfig'
function Show-PSGSuiteConfig {
    <#
    .SYNOPSIS
    Returns the currently loaded config
     
    .DESCRIPTION
    Returns the currently loaded config
     
    .EXAMPLE
    Show-PSGSuiteConfig
    #>

    [CmdletBinding()]
    Param()
    Write-Verbose "Showing current PSGSuite config"
    $script:PSGSuite
}
Export-ModuleMember -Function 'Show-PSGSuiteConfig'
function Switch-PSGSuiteConfig {
    <#
    .SYNOPSIS
    Switches the active config
 
    .DESCRIPTION
    Switches the active config
 
    .PARAMETER ConfigName
    The friendly name of the config you would like to set as active for the session
 
    .PARAMETER Domain
    The domain name for the config you would like to set as active for the session
 
    .PARAMETER SetToDefault
    If passed, also sets the specified config as the default so it's loaded on the next module import
 
    .EXAMPLE
    Switch-PSGSuiteConfig newCustomer
 
    Switches the config to the "newCustomer" config
    #>

    [CmdletBinding(DefaultParameterSetName = "ConfigName",PositionalBinding = $false)]
    Param (
        [parameter(Mandatory = $true,ParameterSetName = "Domain")]
        [ValidateNotNullOrEmpty()]
        [String]
        $Domain,
        [parameter(Mandatory = $false)]
        [switch]
        $SetToDefault
    )
    DynamicParam {
        $attribute = New-Object System.Management.Automation.ParameterAttribute
        $attribute.Position = 0
        $attribute.Mandatory = $true
        $attribute.ParameterSetName = "ConfigName"
        $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $attributeCollection.Add($attribute)
        $names = Get-PSGSuiteConfigNames -Verbose:$false
        $attributeCollection.Add((New-Object  System.Management.Automation.ValidateSetAttribute($names)))
        $parameter = New-Object System.Management.Automation.RuntimeDefinedParameter('ConfigName', [String], $attributeCollection)
        $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $paramDictionary.Add('ConfigName', $parameter)
        return $paramDictionary
    }
    Process {
        $ConfigName = $PSBoundParameters['ConfigName']
        if ($script:PSGSuite.Domain -eq $Domain) {
            Write-Verbose "Current config is already set to domain '$Domain' --- retaining current config. If you would like to import a different config for the same domain, please use the -ConfigName parameter instead"
            if ($SetToDefault) {
                Write-Verbose "Setting config name '$($script:PSGSuite.ConfigName)' for domain '$($script:PSGSuite.Domain)' as default"
                Set-PSGSuiteConfig -ConfigName $($script:PSGSuite.ConfigName) -SetAsDefaultConfig -Verbose:$false
            }
        }
        elseif ($script:PSGSuite.ConfigName -eq $ConfigName) {
            Write-Verbose "Current config is already set to '$ConfigName' --- retaining current config"
            if ($SetToDefault) {
                Write-Verbose "Setting config name '$($script:PSGSuite.ConfigName)' for domain '$($script:PSGSuite.Domain)' as default"
                Set-PSGSuiteConfig -ConfigName $($script:PSGSuite.ConfigName) -SetAsDefaultConfig -Verbose:$false
            }
        }
        else {
            $fullConf = Import-SpecificConfiguration -CompanyName 'SCRT HQ' -Name 'PSGSuite' -Scope $Script:ConfigScope -Verbose:$false
            $defaultConfigName = $fullConf['DefaultConfig']
            $choice = switch ($PSCmdlet.ParameterSetName) {
                Domain {
                    Write-Verbose "Switching active domain to '$Domain'"
                    $fullConf.Keys | Where-Object {(Decrypt $fullConf[$_]['Domain']) -eq $Domain}
                }
                ConfigName {
                    Write-Verbose "Switching active config to '$ConfigName'"
                    $fullConf.Keys | Where-Object {$_ -eq $ConfigName}
                }
            }
            if ($choice) {
                $script:PSGSuite = [PSCustomObject]($fullConf[$choice]) | Get-GSDecryptedConfig -ConfigName $choice
                if ($SetToDefault) {
                    if ($defaultConfigName -ne $choice) {
                        Write-Verbose "Setting config name '$choice' for domain '$($script:PSGSuite.Domain)' as default"
                        Set-PSGSuiteConfig -ConfigName $choice -SetAsDefaultConfig -Verbose:$false
                        $env:PSGSuiteDefaultDomain = $script:PSGSuite.Domain
                        [Environment]::SetEnvironmentVariable("PSGSuiteDefaultDomain", $script:PSGSuite.Domain, "User")
                    }
                    else {
                        Write-Warning "Config name '$choice' for domain '$($script:PSGSuite.Domain)' is already set to default --- no action taken"
                    }
                }
            }
            else {
                switch ($PSCmdlet.ParameterSetName) {
                    Domain {
                        Write-Warning "No config found for domain '$Domain'! Retaining existing config for domain '$($script:PSGSuite.Domain)'"
                    }
                    ConfigName {
                        Write-Warning "No config named '$ConfigName' found! Retaining existing config '$($script:PSGSuite.ConfigName)'"
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Switch-PSGSuiteConfig'
Function Get-GSContactList {
    <#
    .SYNOPSIS
    Gets all contacts for the specified user
 
    .DESCRIPTION
    Gets all contacts for the specified user
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config.
 
    .EXAMPLE
    Get-GSContactList -User user@domain.com
 
    #>

    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        foreach ($U in $User) {
            try {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                $Token = Get-GSToken -Scopes 'https://www.google.com/m8/feeds' -AdminEmail $U
                $Uri = "https://www.google.com/m8/feeds/contacts/$($U)/full?max-results=5000"
                $headers = @{
                    Authorization = "Bearer $($Token)"
                    'GData-Version' = '3.0'
                }
                Write-Verbose "Getting all contacts for user '$U'"
                $Raw = @()
                do {
                    $Response = Invoke-WebRequest -Method Get -Uri ([Uri]$Uri) -Headers $headers -ContentType 'application/xml' -Verbose:$false
                    $Feed = [xml]$Response.Content
                    $Raw += $feed.feed.entry
                    $Uri = $Feed.Feed.Link | Where-Object {$_.rel -eq "next"} | Select-Object -ExpandProperty Href
                    Write-Verbose "Retrieved $($Raw.Count) contacts..."
                }
                until (-not $Uri)
                If ($Raw) {
                    ForEach ($i in $Raw) {
                        [PSCustomObject]@{
                            User           = $U
                            Id             = ($i.id.Split("/")[-1])
                            Title          = $i.title
                            FullName       = $i.name.fullName
                            GivenName      = $i.name.givenName
                            FamilyName     = $i.name.familyName
                            EmailAddresses = $(if($i.email.address){$i.email.address}else{$null})
                            PhoneNumber    = $i.phonenumber
                            Updated        = $i.updated
                            Edited         = $i.edited.'#text'
                            Path           = $(if($i.email.rel){$i.email.rel}else{$null})
                            Etag           = $i.etag
                            FullObject     = $i
                        }
                    }
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSContactList'
Function Remove-GSContact {
    <#
    .SYNOPSIS
    Removes the specified contact
 
    .DESCRIPTION
    Removes the specified contact
 
    .PARAMETER ContactID
    The ContactID to be removed.
 
    .PARAMETER User
    The primary email or UserID of the user. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config.
 
    .PARAMETER Etag
    The Etag string from a Get-GSContactList object. Used to ensure that no changes have been made to the contact since it was viewed in order to prevent data loss.
 
    Defaults to special Etag value *, which can be used to bypass this verification and process the update regardless of updates from other clients.
 
    .EXAMPLE
    Recommended to use Get-GSContactList to find and pipe desired contacts to Remove-GSContact:
 
    Get-GSContactList -User user@domain.com | Where-Object {"@baddomain.com" -match $_.EmailAddresses} | Remove-GSContact
 
    Removes all contacts for user@domain.com that have an email address on the baddomain.com domain.
 
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $false, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Alias("Id")]
        [string[]]
        $ContactId,
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Etag = '*'
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $Token = Get-GSToken -Scopes 'https://www.google.com/m8/feeds' -AdminEmail $User
        $headers = @{
            Authorization   = "Bearer $($Token)"
            'GData-Version' = '3.0'
            'If-Match'      = $Etag
        }
        foreach ($Id in $ContactID) {
            if ($PSCmdlet.ShouldProcess("Removing contact ID '$Id' for $User")) {
                Write-Verbose "Removing contact ID '$Id' for $User"
                try {
                    $Uri = "https://www.google.com/m8/feeds/contacts/$($User)/full/$($Id)"
                    $Response = Invoke-WebRequest -Method "Delete" -Uri ([Uri]$Uri) -Headers $headers -Verbose:$false
                    If ($Response.StatusCode -eq "200") {
                        Write-Verbose "Successfully deleted contact ID '$Id' for $User"
                    }
                    Else {
                        Write-Verbose "HTTP $($Response.StatusCode): $($Response.StatusDescription)"
                    }
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSContact'
function Get-GSCustomer {
    <#
    .SYNOPSIS
    Retrieves a customer
 
    .DESCRIPTION
    Retrieves a customer
 
    .PARAMETER CustomerKey
    Id of the Customer to be retrieved
 
    .EXAMPLE
    Get-GSCustomer (Get-GSUser).CustomerId
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Customer')]
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('CustomerId')]
        [String]
        $CustomerKey = $Script:PSGSuite.CustomerId
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.customer'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Getting Customer '$CustomerKey'"
            $request = $service.Customers.Get($CustomerKey)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSCustomer'
function Update-GSCustomer {
    <#
    .SYNOPSIS
    Updates a customer using patch semantics.
 
    .DESCRIPTION
    Updates a customer using patch semantics.
 
    .PARAMETER CustomerKey
    Id of the Customer to be updated.
 
    .PARAMETER AlternateEmail
    The customer's secondary contact email address. This email address cannot be on the same domain as the customerDomain.
 
    .PARAMETER CustomerDomain
    The customer's primary domain name string. Do not include the www prefix when creating a new customer.
 
    .PARAMETER Language
    The customer's ISO 639-2 language code. The default value is en-US.
 
    .PARAMETER PhoneNumber
    The customer's contact phone number in E.164 format.
 
    .PARAMETER PostalAddress
    The customer's postal address information.
 
    Must be type [Google.Apis.Admin.Directory.directory_v1.Data.CustomerPostalAddress]. Use helper function Add-GSCustomerPostalAddress to create the correct type easily.
 
    .EXAMPLE
    Get-GSCustomer (Get-GSUser).CustomerId
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Customer')]
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('CustomerId')]
        [String]
        $CustomerKey = $Script:PSGSuite.CustomerId,
        [Parameter()]
        [String]
        $AlternateEmail,
        [Parameter()]
        [String]
        $CustomerDomain,
        [Parameter()]
        [String]
        $Language,
        [Parameter()]
        [String]
        $PhoneNumber,
        [Parameter()]
        [Google.Apis.Admin.Directory.directory_v1.Data.CustomerPostalAddress]
        $PostalAddress
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.customer'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Customer'
            foreach ($key in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                $body.$key = $PSBoundParameters[$key]
            }
            Write-Verbose "Updating Customer '$CustomerKey'"
            $request = $service.Customers.Patch($body,$CustomerKey)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSCustomer'
function Get-GSDataTransfer {
    <#
    .SYNOPSIS
    Gets the list of Data Transfers
 
    .DESCRIPTION
    Gets the list of Data Transfers
 
    .PARAMETER DataTransferId
    The Id of the Data Transfer you would like to return info for specifically. Exclude to return the full list
 
    .PARAMETER Status
    Status of the transfer.
 
    .PARAMETER NewOwnerUserId
    Destination user's profile ID.
 
    .PARAMETER OldOwnerUserId
    Source user's profile ID.
 
    .PARAMETER CustomerId
    Immutable ID of the G Suite account.
 
    .PARAMETER PageSize
    PageSize of the result set.
 
    Defaults to 500 (although it's typically a much smaller number for most Customers)
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSDataTransfer
 
    Gets the list of current Data Transfers
    #>

    [OutputType('Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.ApplicationDataTransfer')]
    [cmdletbinding(DefaultParameterSetName = 'List')]
    Param (
        [parameter(Mandatory,Position = 0,ParameterSetName = 'Get')]
        [String[]]
        $DataTransferId,
        [parameter(ParameterSetName = 'List')]
        [String]
        $Status,
        [parameter(ParameterSetName = 'List')]
        [String]
        $NewOwnerUserId,
        [parameter(ParameterSetName = 'List')]
        [String]
        $OldOwnerUserId,
        [parameter(ParameterSetName = 'List')]
        [String]
        $CustomerId = $Script:PSGSuite.CustomerID,
        [parameter(ParameterSetName = 'List')]
        [ValidateRange(1,500)]
        [Int]
        $PageSize = 500,
        [parameter(ParameterSetName = 'List')]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Process {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.datatransfer'
            ServiceType = 'Google.Apis.Admin.DataTransfer.datatransfer_v1.DataTransferService'
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($PSCmdlet.ParameterSetName -eq 'Get') {
                foreach ($I in $DataTransferId) {
                    $request = $service.Transfers.Get($I)
                    $request.Execute()
                }
            }
            else {
                $request = $service.Transfers.List()
                $request.CustomerId = $CustomerId
                if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                    $PageSize = $Limit
                }
                $request.MaxResults = $PageSize
                foreach ($prop in @('NewOwnerUserId','OldOwnerUserId','Status')) {
                    if ($PSBoundParameters.ContainsKey($prop)) {
                        $request.$prop = $PSBoundParameters[$prop]
                    }
                }
                Write-Verbose "Getting Data Transfer list"
                $response = @()
                [int]$i = 1
                $overLimit = $false
                do {
                    $result = $request.Execute()
                    $response += $result.DataTransfers
                    if ($result.NextPageToken) {
                        $request.PageToken = $result.NextPageToken
                    }
                    [int]$retrieved = ($i + $result.DataTransfers.Count) - 1
                    Write-Verbose "Retrieved $retrieved Data Transfers..."
                    if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                        Write-Verbose "Limit reached: $Limit"
                        $overLimit = $true
                    }
                    elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                        $newPS = $Limit - $retrieved
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                        $request.MaxResults = $newPS
                    }
                    [int]$i = $i + $result.DataTransfers.Count
                }
                until ($overLimit -or !$result.NextPageToken)
                return $response
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDataTransfer'
function Get-GSDataTransferApplication {
    <#
    .SYNOPSIS
    Gets the list of available Data Transfer Applications and their parameters
 
    .DESCRIPTION
    Gets the list of available Data Transfer Applications and their parameters
 
    .PARAMETER ApplicationId
    The Application Id of the Data Transfer Application you would like to return info for specifically. Exclude to return the full list
 
    .PARAMETER PageSize
    PageSize of the result set.
 
    Defaults to 500 (although it's typically a much smaller number for most Customers)
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSDataTransferApplication
 
    Gets the list of available Data Transfer Applications
    #>

    [OutputType('Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.Application')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [String[]]
        $ApplicationId,
        [parameter(Mandatory = $false)]
        [ValidateRange(1,500)]
        [Int]
        $PageSize = 500,
        [parameter(Mandatory = $false)]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.datatransfer'
            ServiceType = 'Google.Apis.Admin.DataTransfer.datatransfer_v1.DataTransferService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            if ($ApplicationId) {
                foreach ($I in $ApplicationId) {
                    $request = $service.Applications.Get($I)
                    $request.Execute()
                }
            }
            else {
                $request = $service.Applications.List()
                $request.CustomerId = $Script:PSGSuite.CustomerID
                if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                    $PageSize = $Limit
                }
                $request.MaxResults = $PageSize
                Write-Verbose "Getting all Data Transfer Applications"
                $response = @()
                [int]$i = 1
                $overLimit = $false
                do {
                    $result = $request.Execute()
                    $response += $result.Applications
                    if ($result.NextPageToken) {
                        $request.PageToken = $result.NextPageToken
                    }
                    [int]$retrieved = ($i + $result.Applications.Count) - 1
                    Write-Verbose "Retrieved $retrieved Data Transfer Applications..."
                    if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                        Write-Verbose "Limit reached: $Limit"
                        $overLimit = $true
                    }
                    elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                        $newPS = $Limit - $retrieved
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                        $request.MaxResults = $newPS
                    }
                    [int]$i = $i + $result.Applications.Count
                }
                until ($overLimit -or !$result.NextPageToken)
                return $response
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDataTransferApplication'
function Start-GSDataTransfer {
    <#
    .SYNOPSIS
    Starts a Data Transfer from one user to another
 
    .DESCRIPTION
    Starts a Data Transfer from one user to another
 
    .PARAMETER OldOwnerUserId
    The email or unique Id of the owner you are transferring data *FROM*
 
    .PARAMETER NewOwnerUserId
    The email or unique Id of the owner you are transferring data *TO*
 
    .PARAMETER ApplicationId
    The application Id that you would like to transfer data for
 
    .PARAMETER PrivacyLevel
    The privacy level for the data you'd like to transfer
 
    *Valid for Drive & Docs data transfers only.*
 
    Available values are:
    * "SHARED": all shared content owned by the user
    * "PRIVATE": all private (unshared) content owned by the user
 
    .PARAMETER ReleaseResources
    If true, releases Calendar resources booked by the OldOwner to the NewOwner.
 
    *Valid for Calendar data transfers only.*
 
    .EXAMPLE
    Start-GSDataTransfer -OldOwnerUserId joe -NewOwnerUserId mark -ApplicationId 55656082996 -PrivacyLevel SHARED,PRIVATE
 
    Transfers all of Joe's data to Mark
    #>

    [OutputType('Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.DataTransfer')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory=$true,Position=0)]
        [string]
        $OldOwnerUserId,
        [parameter(Mandatory=$true,Position=1)]
        [string]
        $NewOwnerUserId,
        [parameter(Mandatory=$true,Position=2,ValueFromPipelineByPropertyName=$true)]
        [alias("id")]
        [string]
        $ApplicationId,
        [parameter(Mandatory=$false)]
        [ValidateSet("SHARED","PRIVATE")]
        [string[]]
        $PrivacyLevel,
        [parameter(Mandatory=$false)]
        [switch]
        $ReleaseResources
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.datatransfer'
            ServiceType = 'Google.Apis.Admin.DataTransfer.datatransfer_v1.DataTransferService'
        }
        $service = New-GoogleService @serviceParams
        $OldOwnerUserId = try {
            [bigint]$OldOwnerUserId
        }
        catch {
            Write-Verbose "Resolving Old Owner's UserId from '$OldOwnerUserId'"
            [bigint](Get-GSUser -User $OldOwnerUserId -Projection Basic -Verbose:$false | Select-Object -ExpandProperty id)
        }
        $NewOwnerUserId = try {
            [bigint]$NewOwnerUserId
        }
        catch {
            Write-Verbose "Resolving New Owner's UserId from '$NewOwnerUserId'"
            [bigint](Get-GSUser -User $NewOwnerUserId -Projection Basic -Verbose:$false | Select-Object -ExpandProperty id)
        }
    }
    Process {
        try {
            $body = New-Object 'Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.DataTransfer' -Property @{
                OldOwnerUserId = $OldOwnerUserId
                NewOwnerUserId = $NewOwnerUserId
            }
            $AppDataTransfers = New-Object 'Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.ApplicationDataTransfer' -Property @{
                ApplicationId = $ApplicationId
            }
            if ($PrivacyLevel) {
                $AppDataTransfers.ApplicationTransferParams = [Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.ApplicationTransferParam[]](New-Object 'Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.ApplicationTransferParam' -Property @{
                    Key = 'PRIVACY_LEVEL'
                    Value = [String[]]$PrivacyLevel
                })
            }
            elseif ($ReleaseResources) {
                $AppDataTransfers.ApplicationTransferParams = [Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.ApplicationTransferParam[]](New-Object 'Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.ApplicationTransferParam' -Property @{
                    Key = 'RELEASE_RESOURCES'
                    Value = [String[]]'TRUE'
                })
            }
            $body.ApplicationDataTransfers = [Google.Apis.Admin.DataTransfer.datatransfer_v1.Data.ApplicationDataTransfer[]]$AppDataTransfers
            $request = $service.Transfers.Insert($body)
            Write-Verbose "Starting Data Transfer from User Id '$OldOwnerUserId' to User Id '$NewOwnerUserId'"
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Start-GSDataTransfer'
function Add-GSGmailDelegate {
    <#
    .SYNOPSIS
    Adds a delegate with its verification status set directly to accepted, without sending any verification email. The delegate user must be a member of the same G Suite organization as the delegator user.
 
    .DESCRIPTION
    Adds a delegate with its verification status set directly to accepted, without sending any verification email. The delegate user must be a member of the same G Suite organization as the delegator user.
 
    Gmail imposes limtations on the number of delegates and delegators each user in a G Suite organization can have. These limits depend on your organization, but in general each user can have up to 25 delegates and up to 10 delegators.
 
    Note that a delegate user must be referred to by their primary email address, and not an email alias.
 
    Also note that when a new delegate is created, there may be up to a one minute delay before the new delegate is available for use.
 
    .PARAMETER User
    User's email address to delegate access to.
 
    .PARAMETER Delegate
    Delegate's email address to receive delegate access.
 
    .EXAMPLE
    Add-GSGmailDelegate -User tony@domain.com -Delegate peter@domain.com
 
    Provide Peter delegate access to Tony's inbox.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Delegate')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [Alias("From","Delegator")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory = $true,Position = 1)]
        [Alias("To")]
        [ValidateNotNullOrEmpty()]
        [String]
        $Delegate
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        if ($Delegate -notlike "*@*.*") {
            $Delegate = "$($Delegate)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.sharing'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Adding delegate access to user '$User's inbox for delegate '$Delegate'"
            $body = New-Object 'Google.Apis.Gmail.v1.Data.Delegate' -Property @{
                DelegateEmail = $Delegate
            }
            $request = $service.Users.Settings.Delegates.Create($body,$User)
            $request.Execute()
        }
        catch {
            $origError = $_
            if ($group = Get-GSGroup -Group $User -Verbose:$false -ErrorAction SilentlyContinue) {
                Write-Warning "$User is a group email, not a user account. You can only manage delegate access for a user's inbox. Please add $Delegate to the group $User instead."
            }
            elseif ($group = Get-GSGroup -Group $Delegate -Verbose:$false -ErrorAction SilentlyContinue) {
                Write-Warning "$Delegate is a group email, not a user account. You can only delegate access to other users."
            }
            else {
                $dele = Get-GSGmailDelegates -User $User -NoGroupCheck -ErrorAction SilentlyContinue -Verbose:$false
                if ($dele.DelegateEmail -contains $Delegate -and $dele.VerificationStatus -eq 'accepted') {
                    Write-Warning "'$Delegate' already has delegate access to user '$User's inbox. No action needed."
                }
                elseif ($dele.DelegateEmail -contains $Delegate -and $dele.VerificationStatus -ne 'accepted') {
                    Write-Warning "$Delegate was already invited for delegated access to user '$User's inbox, but VerificationStatus is currently '$($dele.VerificationStatus)'"
                }
                else {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($origError)
                    }
                    else {
                        Write-Error $origError
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSGmailDelegate'
function Get-GSGmailDelegate {
    <#
    .SYNOPSIS
    Gets delegates for the specified account.
 
    .DESCRIPTION
    Gets delegates for the specified account.
 
    .PARAMETER User
    User's email to get delegates for.
 
    .PARAMETER Delegate
    The specific delegate to get. If excluded returns the list of delegates for the user.
 
    .PARAMETER NoGroupCheck
    By default, this will check if the User email is a group email which cannot be delegated if the attempt to delegate access fails.
 
    Include this switch to prevent the group check and return the original error.
 
    .EXAMPLE
    Get-GSGmailDelegate -User tony@domain.com
 
    Gets the list of users who have delegate access to Tony's inbox.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Delegate')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [Alias("From","Delegator")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,Position = 1)]
        [Alias("To")]
        [ValidateNotNullOrEmpty()]
        [String]
        $Delegate,
        [parameter(Mandatory = $false)]
        [switch]
        $NoGroupCheck
    )
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
                ServiceType = 'Google.Apis.Gmail.v1.GmailService'
                User        = $U
            }
            $service = New-GoogleService @serviceParams
            if ($PSBoundParameters.Keys -contains 'Delegate') {
                try {
                    Write-Verbose "Getting Gmail Delegate '$Delegate' for user '$U'"
                    $request = $service.Users.Settings.Delegates.Get($U,$Delegate)
                    $request.Execute()
                }
                catch {
                    $origError = $_
                    if (!$NoGroupCheck -and ($group = Get-GSGroup -Group $U -Verbose:$false -ErrorAction SilentlyContinue)) {
                        Write-Warning "$U is a group, not a user. You can only manage delegates for a user."
                    }
                    else {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($origError)
                        }
                        else {
                            Write-Error $origError
                        }
                    }
                }
            }
            else {
                try {
                    Write-Verbose "Getting Gmail Delegate list for user '$U'"
                    $request = $service.Users.Settings.Delegates.List($U)
                    $res = $request.Execute()
                    if ($res.Delegates) {
                        $res.Delegates | Add-Member -MemberType NoteProperty -Name Delegator -Value $U -Force -PassThru
                    }
                    else {
                        Write-Warning "No delegates found for user '$U'"
                    }
                }
                catch {
                    $origError = $_
                    if (!$NoGroupCheck -and ($group = Get-GSGroup -Group $U -Verbose:$false -ErrorAction SilentlyContinue)) {
                        Write-Warning "$U is a group, not a user. You can only manage delegates for a user."
                    }
                    else {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($origError)
                        }
                        else {
                            Write-Error $origError
                        }
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailDelegate'
function Remove-GSGmailDelegate {
    <#
    .SYNOPSIS
    Removes the specified delegate (which can be of any verification status), and revokes any verification that may have been required for using it.
 
    .DESCRIPTION
    Removes the specified delegate (which can be of any verification status), and revokes any verification that may have been required for using it.
 
    Note that a delegate user must be referred to by their primary email address, and not an email alias.
 
    .PARAMETER User
    User's email address to remove delegate access to
 
    .PARAMETER Delegate
    Delegate's email address to remove
 
    .EXAMPLE
    Remove-GSGmailDelegate -User tony@domain.com -Delegate peter@domain.com
 
    Removes Peter's access to Tony's inbox.
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [Alias("From","Delegator")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory = $true,Position = 1)]
        [Alias("To")]
        [ValidateNotNullOrEmpty()]
        [String]
        $Delegate
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        if ($Delegate -notlike "*@*.*") {
            $Delegate = "$($Delegate)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.sharing'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        if ($PSCmdlet.ShouldProcess("Removing delegate access for '$Delegate' from user '$User's inbox")) {
            try {
                Write-Verbose "Removing delegate access for '$Delegate' from user '$User's inbox"
                $request = $service.Users.Settings.Delegates.Delete($User,$Delegate)
                $request.Execute()
                Write-Verbose "Successfully removed delegate access for user '$User's inbox for delegate '$Delegate'"
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($origError)
                }
                else {
                    Write-Error $origError
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGmailDelegate'
function Get-GSDomain {
    <#
    .SYNOPSIS
    Retrieves a Domain
 
    .DESCRIPTION
    Retrieves a Domain
 
    .PARAMETER DomainName
    Name of the domain to retrieve.
 
    If excluded, returns the list of domains.
 
    .EXAMPLE
    Get-GSDDomain
 
    Returns the list of domains.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Domains')]
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('Domain')]
        [String[]]
        $DomainName
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.domain'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        if ($PSBoundParameters.ContainsKey('DomainName')) {
            foreach ($domain in $DomainName) {
                try {
                    Write-Verbose "Getting Domain '$domain'"
                    $request = $service.Domains.Get($Script:PSGSuite.CustomerId,$domain)
                    $request.Execute()
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
        else {
            try {
                Write-Verbose "Getting the list of Domains"
                $request = $service.Domains.List($Script:PSGSuite.CustomerId)
                $request.Execute().Domains
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDomain'
function Get-GSDomainAlias {
    <#
    .SYNOPSIS
    Retrieves a Domain Alias
 
    .DESCRIPTION
    Retrieves a Domain Alias
 
    .PARAMETER DomainAliasName
    Name of the domain alias to retrieve.
 
    If excluded, returns the list of domain aliases.
 
    .PARAMETER ParentDomainName
    Name of the parent domain to list aliases for.
 
    If excluded, lists all aliases for all domains.
 
    .EXAMPLE
    Get-GSDDomainAlias
 
    Returns the list of domain aliases for all domains.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.DomainAlias')]
    [CmdletBinding(DefaultParameterSetName = "List")]
    Param(
        [Parameter(Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName = "Get")]
        [Alias('DomainAlias')]
        [String[]]
        $DomainAliasName,
        [Parameter(Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName = "List")]
        [String[]]
        $ParentDomainName
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.domain'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        if ($PSBoundParameters.ContainsKey('DomainAliasName')) {
            foreach ($alias in $DomainAliasName) {
                try {
                    Write-Verbose "Getting DomainAlias '$alias'"
                    $request = $service.DomainAliases.Get($Script:PSGSuite.CustomerId,$alias)
                    $request.Execute()
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
        else {
            try {
                $request = $service.DomainAliases.List($Script:PSGSuite.CustomerId)
                if ($PSBoundParameters.ContainsKey('ParentDomainName')) {
                    foreach ($pDom in $ParentDomainName) {
                        Write-Verbose "Getting the list of all DomainAliases under parent domain '$pDom'"
                        $request.ParentDomainName = $pDom
                        $request.Execute().DomainAliasesValue
                    }
                }
                else {
                    Write-Verbose "Getting the list of all DomainAliases"
                    $request.Execute().DomainAliasesValue
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDomainAlias'
function New-GSDomain {
    <#
    .SYNOPSIS
    Adds a new Domain
 
    .DESCRIPTION
    Adds a new Domain
 
    .PARAMETER DomainName
    Name of the domain to add.
 
    .EXAMPLE
    New-GSDDomain -DomainName 'testing.com'
 
    Adds a new domain named 'testing.com'
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Domains')]
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('Domain')]
        [String]
        $DomainName
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.domain'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Adding Domain '$DomainName'"
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Domains' -Property @{
                DomainName = $DomainName
            }
            $request = $service.Domains.Insert($body,$Script:PSGSuite.CustomerId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSDomain'
function New-GSDomainAlias {
    <#
    .SYNOPSIS
    Adds a new Domain Alias
 
    .DESCRIPTION
    Adds a new Domain Alias
 
    .PARAMETER DomainAliasName
    Name of the domain alias to add.
 
    .PARAMETER ParentDomainName
    Name of the parent domain to add the alias for.
 
    .EXAMPLE
    New-GSDDomainAlias -DomainAliasName 'testingalias.com' -ParentDomainName 'testing.com'
 
    Adds a new domain alias named 'testingalias.com' to parent domain 'testing.com'
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.DomainAlias')]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('DomainAlias')]
        [String]
        $DomainAliasName,
        [Parameter(Mandatory,Position = 1,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [String]
        $ParentDomainName
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.domain'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Adding DomainAlias '$DomainAliasName' to domain '$ParentDomainName'"
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.DomainAlias' -Property @{
                DomainAliasName = $DomainAliasName
                ParentDomainName = $ParentDomainName
            }
            $request = $service.DomainAliases.Insert($body,$Script:PSGSuite.CustomerId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSDomainAlias'
function Remove-GSDomain {
    <#
    .SYNOPSIS
    Removes a Domain
 
    .DESCRIPTION
    Removes a Domain
 
    .PARAMETER DomainName
    Name of the domain to remove.
 
    .EXAMPLE
    Remove-GSDDomain 'testing.com'
 
    Removes the 'testing.com' domain from your account.
    #>

    [CmdletBinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param(
        [Parameter(Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('Domain')]
        [String[]]
        $DomainName
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.domain'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($domain in $DomainName) {
            try {
                if ($PSCmdlet.ShouldProcess("Removing Domain '$domain'")) {
                    Write-Verbose "Removing Domain '$domain'"
                    $request = $service.Domains.Get($Script:PSGSuite.CustomerId,$domain)
                    $request.Execute()
                    Write-Verbose "Domain '$domain' removed successfully"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSDomain'
function Remove-GSDomainAlias {
    <#
    .SYNOPSIS
    Removes a Domain Alias
 
    .DESCRIPTION
    Removes a Domain Alias
 
    .PARAMETER DomainAliasName
    Alias of the domain to remove.
 
    .EXAMPLE
    Remove-GSDDomainAlias 'testingalias.com'
 
    Removes the 'testingalias.com' domain alias from your account.
    #>

    [CmdletBinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param(
        [Parameter(Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('DomainAlias')]
        [String[]]
        $DomainAliasName
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.domain'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($alias in $DomainAliasName) {
            try {
                if ($PSCmdlet.ShouldProcess("Removing Domain Alias '$domain'")) {
                    Write-Verbose "Removing Domain Alias '$alias'"
                    $request = $service.DomainAliases.Delete($Script:PSGSuite.CustomerId,$alias)
                    $request.Execute()
                    Write-Verbose "Domain Alias '$alias' removed successfully"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSDomainAlias'
function Add-GSDocContent {
    <#
    .SYNOPSIS
    Adds content to a Google Doc via appending new text. This does not overwrite existing content
 
    .DESCRIPTION
    Adds content to a Google Doc via appending new text. This does not overwrite existing content
 
    .PARAMETER FileID
    The unique Id of the file to add content to
 
    .PARAMETER Value
    The content to add
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    $newLogStrings | Add-GSDocContent -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976'
 
    Appends the strings in the $newLogStrings variable to the existing the content of the specified Google Doc.
    #>

    [CmdLetBinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $FileID,
        [parameter(Mandatory = $true,ValueFromPipeline = $true)]
        [String[]]
        $Value,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $service = New-GoogleService @serviceParams
        $stream = New-Object 'System.IO.MemoryStream'
        $writer = New-Object 'System.IO.StreamWriter' $stream
        $currentContent = Get-GSDocContent -FileID $FileID -User $User -Verbose:$false
        $concatStrings = @($currentContent)
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        foreach ($string in $Value) {
            $concatStrings += $string
        }
    }
    End {
        try {
            $concatStrings = $concatStrings -join "`n"
            $writer.Write($concatStrings)
            $writer.Flush()
            $contentType = 'text/plain'
            $body = New-Object 'Google.Apis.Drive.v3.Data.File'
            $request = $service.Files.Update($body,$FileId,$stream,$contentType)
            $request.QuotaUser = $User
            $request.ChunkSize = 512KB
            $request.SupportsAllDrives = $true
            Write-Verbose "Adding content to File '$FileID'"
            $request.Upload() | Out-Null
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
        finally {
            if ($stream) {
                $stream.Close()
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSDocContent'
function Add-GSDrivePermission {
    <#
    .SYNOPSIS
    Adds a new permission to a Drive file
 
    .DESCRIPTION
    Adds a new permission to a Drive file
 
    .PARAMETER User
    The owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .PARAMETER FileId
    The unique Id of the Drive file you would like to add the permission to
 
    .PARAMETER Role
    The role/permission set you would like to give the email $EmailAddress
 
    Available values are:
    * "Owner"
    * "Writer"
    * "Commenter"
    * "Reader"
    * "Organizer"
    * "FileOrganizer"
 
    .PARAMETER Type
    The type of the grantee
 
    Available values are:
    * "User": a user email
    * "Group": a group email
    * "Domain": the entire domain
    * "Anyone": public access
 
    .PARAMETER EmailAddress
    The email address of the user or group to which this permission refers
 
    .PARAMETER Domain
    The domain to which this permission refers
 
    .PARAMETER ExpirationTime
    The time at which this permission will expire.
 
    Expiration times have the following restrictions:
    * They can only be set on user and group permissions
    * The time must be in the future
    * The time cannot be more than a year in the future
 
    .PARAMETER EmailMessage
    A plain text custom message to include in the notification email
 
    .PARAMETER SendNotificationEmail
    Whether to send a notification email when sharing to users or groups.
 
    This defaults to **FALSE** for users and groups in PSGSuite, and is not allowed for other requests.
 
    **It must not be disabled for ownership transfers**
 
    .PARAMETER AllowFileDiscovery
    Whether the permission allows the file to be discovered through search.
 
    This is only applicable for permissions of type domain or anyone
 
    .PARAMETER TransferOwnership
    Confirms transfer of ownership if the Role is set to 'Owner'. You can also force the same behavior by passing -Confirm:$false instead
 
    .PARAMETER UseDomainAdminAccess
    Whether the request should be treated as if it was issued by a domain administrator; if set to true, then the requester will be granted access if they are an administrator of the domain to which the item belongs
 
    .EXAMPLE
    Add-GSDrivePermission -FileId "1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976" -Role Owner -Type User -EmailAddress joe -SendNotificationEmail -Confirm:$false
 
    Adds user joe@domain.com as the new owner of the file Id and sets the AdminEmail user as a Writer on the file
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Permission')]
    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High",DefaultParameterSetName = "Email")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String]
        $FileId,
        [parameter(Mandatory = $true)]
        [ValidateSet("Owner","Writer","Commenter","Reader","Organizer","FileOrganizer")]
        [String]
        $Role,
        [parameter(Mandatory = $true)]
        [ValidateSet("User","Group","Domain","Anyone")]
        [String]
        $Type,
        [parameter(Mandatory = $false,Position = 1,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ParameterSetName = "Email")]
        [String]
        $EmailAddress,
        [parameter(Mandatory = $false,ParameterSetName = "Domain")]
        [String]
        $Domain,
        [parameter(Mandatory = $false)]
        [DateTime]
        $ExpirationTime,
        [parameter(Mandatory = $false)]
        [string]
        $EmailMessage,
        [parameter(Mandatory = $false)]
        [Switch]
        $SendNotificationEmail,
        [parameter(Mandatory = $false)]
        [Switch]
        $AllowFileDiscovery,
        [parameter(Mandatory = $false)]
        [Alias('ConfirmTransferOfOwnership','TransferOfOwnership')]
        [switch]
        $TransferOwnership,
        [parameter(Mandatory = $false)]
        [switch]
        $UseDomainAdminAccess
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($Role -eq "Owner" -and !$TransferOwnership) {
                if ($PSCmdlet.ShouldProcess("Confirm transfer of ownership of FileId '$FileID' from user '$User' to user '$EmailAddress'")) {
                    $PSBoundParameters['TransferOwnership'] = $true
                    $TransferOwnership = $true
                }
                else {
                    throw "The TransferOwnership parameter is required when setting the 'Owner' role."
                }
            }
            if (($Type -eq "User" -or $Type -eq "Group") -and !$EmailAddress) {
                throw "The EmailAddress parameter is required for types 'User' or 'Group'."
            }
            if (($Type -eq "User" -or $Type -eq "Group") -and ($PSBoundParameters.Keys -contains 'AllowFileDiscovery')) {
                Write-Warning "The AllowFileDiscovery parameter is only applicable for types 'Domain' or 'Anyone' This parameter will be excluded from this request."
                $PSBoundParameters.Remove('AllowFileDiscovery') | Out-Null
            }
            if ($TransferOwnership -and !$SendNotificationEmail) {
                $PSBoundParameters['SendNotificationEmail'] = $true
                Write-Warning "Setting SendNotificationEmail to 'True' to prevent errors (required for Ownership transfers)"
            }
            $body = New-Object 'Google.Apis.Drive.v3.Data.Permission'
            foreach ($key in $PSBoundParameters.Keys) {
                switch ($key) {
                    EmailAddress {
                        if ($EmailAddress -ceq 'me') {
                            $EmailAddress = $Script:PSGSuite.AdminEmail
                        }
                        elseif ($EmailAddress -notlike "*@*.*") {
                            $EmailAddress = "$($EmailAddress)@$($Script:PSGSuite.Domain)"
                        }
                        $body.EmailAddress = $EmailAddress
                    }
                    Role {
                        $body.$key = ($PSBoundParameters[$key]).ToLower().Replace("fileorganizer","fileOrganizer")
                    }
                    Type {
                        $body.$key = ($PSBoundParameters[$key]).ToLower()
                    }
                    Default {
                        if ($body.PSObject.Properties.Name -contains $key) {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            $request = $service.Permissions.Create($body,$FileId)
            $request.SupportsAllDrives = $true
            foreach ($key in $PSBoundParameters.Keys) {
                if ($request.PSObject.Properties.Name -contains $key -and $key -ne 'FileId') {
                    $request.$key = $PSBoundParameters[$key]
                }
            }
            if ($PSBoundParameters.Keys -notcontains 'SendNotificationEmail') {
                $request.SendNotificationEmail = $false
            }
            Write-Verbose "Adding Drive Permission of '$Role' for user '$EmailAddress' on Id '$FileID'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSDrivePermission'
function Copy-GSDriveFile {
    <#
    .SYNOPSIS
    Make a copy of a file in Drive
 
    .DESCRIPTION
    Make a copy of a file in Drive
 
    .PARAMETER FileID
    The unique Id of the file to copy
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .PARAMETER Name
    The name of the new Drive file copy
 
    .PARAMETER Description
    The description of the new Drive file copy
 
    .PARAMETER Parents
    The parent Ids of the new Drive file copy
 
    .PARAMETER Projection
    The defined subset of fields to be returned
 
    Available values are:
    * "Minimal"
    * "Standard"
    * "Full"
    * "Access"
 
    .PARAMETER Fields
    The specific fields to returned
 
    .EXAMPLE
    Copy-GSDriveFile -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -Name "New Daily Checklist"
 
    Copies the Drive file Id to a new Drive file named 'New Daily Checklist'
    #>

    [OutputType('Google.Apis.Drive.v3.Data.File')]
    [cmdletbinding(DefaultParameterSetName = "Depth")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $FileID,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [String[]]
        $Parents,
        [parameter(Mandatory = $false,ParameterSetName = "Depth")]
        [Alias('Depth')]
        [ValidateSet("Minimal","Standard","Full","Access")]
        [String]
        $Projection = "Full",
        [parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [ValidateSet("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","thumbnailLink","thumbnailVersion","trashed","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")]
        [String[]]
        $Fields
    )
    Begin {
        if ($Projection) {
            $fs = switch ($Projection) {
                Standard {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","owners","parents","properties","version","webContentLink","webViewLink")
                }
                Access {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","ownedByMe","owners","parents","permissionIds","permissions","shared","sharedWithMeTime","sharingUser","viewedByMe","viewedByMeTime","viewersCanCopyContent","writersCanShare")
                }
                Full {
                    @("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasAugmentedPermissions","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissionIds","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","teamDriveId","thumbnailLink","thumbnailVersion","trashed","trashedTime","trashingUser","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")
                }
            }
        }
        elseif ($PSBoundParameters.ContainsKey('Fields')) {
            $fs = $Fields
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Drive.v3.Data.File'
            if ($Name) {
                $body.Name = $Name
            }
            if ($Description) {
                $body.Description = $Description
            }
            if ($Parents) {
                $body.Parents = [String[]]$Parents
            }
            $request = $service.Files.Copy($body,$FileID)
            $request.SupportsAllDrives = $true
            if ($fs) {
                $request.Fields = "$($fs -join ",")"
            }
            Write-Verbose "Copying drive file id '$FileID'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Copy-GSDriveFile'
function Export-GSDriveFile {
    <#
    .SYNOPSIS
    Exports a Drive file as if you chose "Export" from the File menu when viewing the file
 
    .DESCRIPTION
    Exports a Drive file as if you chose "Export" from the File menu when viewing the file
 
    .PARAMETER FileID
    The unique Id of the file to export
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .PARAMETER Type
    The type of local file you would like to export the Drive file as
 
    Available values are:
    * "CSV"
    * "HTML"
    * "JPEG"
    * "JSON"
    * "MSExcel"
    * "MSPowerPoint"
    * "MSWordDoc"
    * "OpenOfficeDoc"
    * "OpenOfficeSheet"
    * "PDF"
    * "PlainText"
    * "PNG"
    * "RichText"
    * "SVG"
 
    .PARAMETER OutFilePath
    The directory path that you would like to export the Drive file to
 
    Defaults to the current working directory
 
    .PARAMETER Projection
    The defined subset of fields to be returned
 
    Available values are:
    * "Minimal"
    * "Standard"
    * "Full"
    * "Access"
 
    .PARAMETER Fields
    The specific fields to returned
 
    .PARAMETER Force
    If $true, overwrites any existing files at the same path.
 
    .EXAMPLE
    Export-GSDriveFile -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -Type CSV -OutFilePath .\SheetExport.csv
 
    Exports the Drive file as a CSV to the current working directory
    #>

    [CmdLetBinding(DefaultParameterSetName = "Depth")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $FileID,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $true)]
        [ValidateSet("CSV","EPUB","HTML","HTMLZipped","JPEG","JSON","MSExcel","MSPowerPoint","MSWordDoc","OpenOfficeDoc","OpenOfficePresentation","OpenOfficeSheet","PDF","PlainText","PNG","RichText","SVG","TSV")]
        [String]
        $Type,
        [parameter(Mandatory = $false)]
        [String]
        $OutFilePath,
        [parameter(Mandatory = $false,ParameterSetName = "Depth")]
        [Alias('Depth')]
        [ValidateSet("Minimal","Standard","Full","Access")]
        [String]
        $Projection = "Full",
        [parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [ValidateSet("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","thumbnailLink","thumbnailVersion","trashed","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")]
        [String[]]
        $Fields,
        [parameter(Mandatory = $false)]
        [Switch]
        $Force
    )
    Begin {
        if ($Projection) {
            $fs = switch ($Projection) {
                Standard {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","owners","parents","properties","version","webContentLink","webViewLink")
                }
                Access {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","ownedByMe","owners","parents","permissionIds","permissions","shared","sharedWithMeTime","sharingUser","viewedByMe","viewedByMeTime","viewersCanCopyContent","writersCanShare")
                }
                Full {
                    @("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasAugmentedPermissions","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissionIds","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","teamDriveId","thumbnailLink","thumbnailVersion","trashed","trashedTime","trashingUser","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")
                }
            }
        }
        elseif ($PSBoundParameters.ContainsKey('Fields')) {
            $fs = $Fields
        }
        $mimeHash = @{
            CSV                    = "text/csv"
            EPUB                   = "application/epub+zip"
            HTML                   = "text/html"
            HTMLZipped             = "application/zip"
            JPEG                   = "image/jpeg"
            JSON                   = "application/vnd.google-apps.script+json"
            MSExcel                = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
            MSPowerPoint           = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
            MSWordDoc              = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
            OpenOfficeDoc          = "application/vnd.oasis.opendocument.text"
            OpenOfficePresentation = "application/vnd.oasis.opendocument.presentation"
            OpenOfficeSheet        = "application/x-vnd.oasis.opendocument.spreadsheet"
            PDF                    = "application/pdf"
            PlainText              = "text/plain"
            PNG                    = "image/png"
            RichText               = "application/rtf"
            SVG                    = "image/svg+xml"
            TSV                    = "text/tab-separated-values"
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $request = $service.Files.Export($FileID,($mimeHash[$Type]))
            if ($fs) {
                $request.Fields = $($fs -join ",")
            }
            if ($OutFilePath) {
                if ((Test-Path $OutFilePath) -and !$Force) {
                    throw "File '$OutFilePath' already exists. If you would like to overwrite it, use the -Force parameter."
                }
                else {
                    Write-Verbose "Saving file to path '$OutFilePath'"
                    $stream = [System.IO.File]::Create($OutFilePath)
                    $request.Download($stream)
                    $stream.Close()
                }
            }
            else {
                Write-Verbose "Getting content of File '$FileID' as Type '$Type'"
                $request.Execute()
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Export-GSDriveFile'
function Get-GSDocContent {
    <#
    .SYNOPSIS
    Gets the content of a Google Doc and returns it as an array of strings. Supports HTML or PlainText
 
    .DESCRIPTION
    Gets the content of a Google Doc and returns it as an array of strings. Supports HTML or PlainText
 
    .PARAMETER FileID
    The unique Id of the file to get content of
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .PARAMETER Type
    Whether to get the results in HTML or PlainText format.
 
    .EXAMPLE
    Get-GSDocContent -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976'
 
    Exports the Drive file as a CSV to the current working directory
    #>

    [CmdLetBinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $FileID,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [ValidateSet("HTML","PlainText")]
        [String]
        $Type
    )
    Begin {
        $typeParam = @{}
        if ($PSBoundParameters.Keys -notcontains 'Type') {
            $typeParam['Type'] = "PlainText"
        }
    }
    Process {
        try {
            (Export-GSDriveFile @PSBoundParameters -Projection Minimal @typeParam) -split "`n"
            Write-Verbose "Content retrieved for File '$FileID'"
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDocContent'
function Get-GSDrive {
    <#
    .SYNOPSIS
    Gets information about a Shared Drive
 
    .DESCRIPTION
    Gets information about a Shared Drive
 
    .PARAMETER DriveId
    The unique Id of the Shared Drive. If excluded, the list of Shared Drives will be returned
 
    .PARAMETER User
    The email or unique Id of the user with access to the Shared Drive
 
    .PARAMETER Filter
    Query string for searching Shared Drives. See the "Search for Files and Shared Drives" guide for the supported syntax: https://developers.google.com/drive/v3/web/search-parameters
 
    PowerShell filter syntax here is supported as "best effort". Please use Google's filter operators and syntax to ensure best results
 
    .PARAMETER UseDomainAdminAccess
    Issue the request as a domain administrator; if set to true, then all Shared Drives of the domain in which the requester is an administrator are returned.
 
    .PARAMETER PageSize
    The page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSDrive -Limit 3 -UseDomainAdminAccess
 
    Gets the first 3 Shared Drives in the domain.
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Drive')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Get")]
        [Alias('Id','TeamDriveId')]
        [String[]]
        $DriveId,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('Q','Query')]
        [String]
        $Filter,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Switch]
        $UseDomainAdminAccess,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateRange(1,100)]
        [Int]
        $PageSize = "100",
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Process {
        if ($UseDomainAdminAccess -or $User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Get {
                    foreach ($id in $DriveId) {
                        $request = $service.Drives.Get($id)
                        Write-Verbose "Getting Shared Drive '$id' for user '$User'"
                        $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                    }
                }
                List {
                    $request = $service.Drives.List()
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.PageSize = $PageSize
                    if ($UseDomainAdminAccess) {
                        $request.UseDomainAdminAccess = $true
                        Write-Verbose "Getting Shared Drives as Domain Admin"
                    }
                    else {
                        Write-Verbose "Getting Shared Drives for user '$User'"
                    }
                    if ($Filter) {
                        $FilterFmt = $Filter -replace " -eq ","=" -replace " -like "," contains " -replace " -match "," contains " -replace " -contains "," contains " -creplace "'True'","True" -creplace "'False'","False" -replace " -in "," in " -replace " -le ",'<=' -replace " -ge ",">=" -replace " -gt ",'>' -replace " -lt ",'<' -replace " -ne ","!=" -replace " -and "," and " -replace " -or "," or " -replace " -not "," not "
                        $request.Q = $($FilterFmt -join " ")
                    }
                    [int]$i = 1
                    $overLimit = $false
                    do {
                        $result = $request.Execute()
                        if ($result.Drives) {
                            $result.Drives | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                        }
                        $request.PageToken = $result.NextPageToken
                        [int]$retrieved = ($i + $result.Drives.Count) - 1
                        Write-Verbose "Retrieved $retrieved Shared Drives..."
                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                            Write-Verbose "Limit reached: $Limit"
                            $overLimit = $true
                        }
                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                            $newPS = $Limit - $retrieved
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                            $request.PageSize = $newPS
                        }
                        [int]$i = $i + $result.Drives.Count
                    }
                    until ($overLimit -or !$result.NextPageToken)
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDrive'
function Get-GSDriveFile {
    <#
    .SYNOPSIS
    Gets information about or downloads a Drive file
 
    .DESCRIPTION
    Gets information about or downloads a Drive file
 
    .PARAMETER FileId
    The unique Id of the file to get
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .PARAMETER OutFilePath
    The directory path that you would like to download the Drive file to. If excluded, only the Drive file information will be returned
 
    .PARAMETER Projection
    The defined subset of fields to be returned
 
    Available values are:
    * "Standard"
    * "Full"
    * "Access"
 
    .PARAMETER Fields
    The specific fields to returned
 
    .PARAMETER Force
    If $true and OutFilePath is specified, overwrites any existing files at the desired path.
 
    .EXAMPLE
    Get-GSDriveFile -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976'
 
    Gets the information for the file
 
    .EXAMPLE
    Get-GSDriveFile -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -OutFilePath (Get-Location).Path
 
    Gets the information for the file and saves the file in the current working directory
    #>

    [OutputType('Google.Apis.Drive.v3.Data.File')]
    [cmdletbinding(DefaultParameterSetName = "Depth")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String[]]
        $FileId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [Alias('SaveFileTo')]
        [String]
        $OutFilePath,
        [parameter(Mandatory = $false,ParameterSetName = "Depth")]
        [Alias('Depth')]
        [ValidateSet("Standard","Full","Access")]
        [String]
        $Projection = "Full",
        [parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [ValidateSet("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","thumbnailLink","thumbnailVersion","trashed","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")]
        [String[]]
        $Fields,
        [parameter(Mandatory = $false)]
        [Switch]
        $Force
    )
    Begin {
        if ($PSCmdlet.ParameterSetName -eq 'Depth') {
            $fs = switch ($Projection) {
                Standard {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","owners","parents","properties","version","webContentLink","webViewLink")
                }
                Access {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","ownedByMe","owners","parents","permissionIds","permissions","shared","sharedWithMeTime","sharingUser","viewedByMe","viewedByMeTime","viewersCanCopyContent","writersCanShare")
                }
                Full {
                    @("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasAugmentedPermissions","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissionIds","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","teamDriveId","thumbnailLink","thumbnailVersion","trashed","trashedTime","trashingUser","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")
                }
            }
        }
        elseif ($PSBoundParameters.ContainsKey('Fields')) {
            $fs = $Fields
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            foreach ($file in $FileId) {
                $backupPath = $null
                $request = $service.Files.Get($file)
                $request.SupportsAllDrives = $true
                if ($fs) {
                    $request.Fields = $($fs -join ",")
                }
                $res = $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                if ($OutFilePath) {
                    if (Test-Path $OutFilePath) {
                        $outFilePathItem = Get-Item $OutFilePath -ErrorAction SilentlyContinue
                        if ($outFilePathItem.PSIsContainer) {
                            $resPath = $outFilePathItem.FullName
                            $cleanedName = Get-SafeFileName "$($res.Name).$($res.FileExtension)"
                            $filePath = Join-Path $resPath $cleanedName
                        }
                        elseif ($Force) {
                            $endings = [System.Collections.Generic.List[string]]@('.bak')
                            1..100 | ForEach-Object {$endings.Add(".bak$_")}
                            foreach ($end in $endings) {
                                $backupPath = "$($outFilePathItem.FullName)$end"
                                if (-not (Test-Path $backupPath)) {
                                    break
                                }
                                else {
                                    $backupPath = $null
                                }
                            }
                            Write-Warning "Renaming '$($outFilePathItem.Name)' to '$($outFilePathItem.Name).bak' in case replacement download fails."
                            Rename-Item $outFilePathItem.FullName -NewName $backupPath -Force
                            $filePath = $OutFilePath
                        }
                        else {
                            throw "File already exists at path '$($OutFilePath)'. Please specify -Force to overwrite any files with the same name if they exist."
                        }
                    }
                    else {
                        $filePath = $OutFilePath
                    }
                    Write-Verbose "Saving file to path '$filePath'"
                    $stream = [System.IO.File]::Create($filePath)
                    $request.Download($stream)
                    $stream.Close()
                    $res | Add-Member -MemberType NoteProperty -Name OutFilePath -Value $filePath -Force
                    if ($backupPath) {
                        Write-Verbose "File has been downloaded successfully! Removing the backup file at path: $backupPath"
                        Remove-Item $backupPath -Recurse -Force -Confirm:$false
                    }
                }
                $res
            }
        }
        catch {
            $err = $_
            if ($backupPath) {
                if (Test-Path $outFilePathItem.FullName) {
                    Remove-Item $outFilePathItem.FullName -Recurse -Force
                }
                Rename-Item $backupPath -NewName $outFilePathItem.FullName -Force
            }
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($err)
            }
            else {
                Write-Error $err
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDriveFile'
function Get-GSDriveFileList {
    <#
    .SYNOPSIS
    Gets the list of Drive files owned by the user
 
    .DESCRIPTION
    Gets the list of Drive files owned by the user
 
    .PARAMETER User
    The email or unique Id of the user whose Drive files you are trying to list
 
    Defaults to the AdminEmail user
 
    .PARAMETER Filter
    A query for filtering the file results. See the "Search for Files and Team Drives" guide for the supported syntax: https://developers.google.com/drive/v3/web/search-parameters
 
    PowerShell filter syntax here is supported as "best effort". Please use Google's filter operators and syntax to ensure best results
 
    .PARAMETER TeamDriveId
    ID of Team Drive to search
 
    .PARAMETER ParentFolderId
    ID of parent folder to search to add to the filter
 
    .PARAMETER Recurse
    If True, recurses through subfolders found underneath primary search results
 
    .PARAMETER IncludeTeamDriveItems
    Whether Team Drive items should be included in results. (Default: false)
 
    .PARAMETER Corpora
    Comma-separated list of bodies of items (files/documents) to which the query applies. Supported bodies are 'User', 'Domain', 'TeamDrive' and 'AllTeamDrives'. 'AllTeamDrives' must be combined with 'User'; all other values must be used in isolation. Prefer 'User' or 'TeamDrive' to 'AllTeamDrives' for efficiency.
 
    .PARAMETER Fields
    The specific fields to fetch for the listed files.
 
    .PARAMETER Spaces
    A comma-separated list of spaces to query within the corpus. Supported values are 'Drive', 'AppDataFolder' and 'Photos'.
 
    .PARAMETER OrderBy
    A comma-separated list of sort keys. Valid keys are 'createdTime', 'folder', 'modifiedByMeTime', 'modifiedTime', 'name', 'name_natural', 'quotaBytesUsed', 'recency', 'sharedWithMeTime', 'starred', and 'viewedByMeTime'.
 
    .PARAMETER PageSize
    The page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSDriveFileList joe
 
    Gets Joe's Drive file list
    #>

    [OutputType('Google.Apis.Drive.v3.Data.File')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [Alias('Q','Query')]
        [String[]]
        $Filter,
        [parameter(Mandatory = $false)]
        [String]
        $TeamDriveId,
        [parameter(Mandatory = $false)]
        [String]
        $ParentFolderId,
        [parameter(Mandatory = $false)]
        [Switch]
        $Recurse,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeTeamDriveItems,
        [parameter(Mandatory = $false)]
        [String[]]
        $Fields = @('files','kind','nextPageToken'),
        [parameter(Mandatory = $false)]
        [ValidateSet('user','domain','teamDrive')]
        [String]
        $Corpora,
        [parameter(Mandatory = $false)]
        [ValidateSet('drive','appDataFolder','photos')]
        [String[]]
        $Spaces,
        [parameter(Mandatory = $false)]
        [ValidateSet('createdTime','folder','modifiedByMeTime','modifiedTime','name','quotaBytesUsed','recency','sharedWithMeTime','starred','viewedByMeTime')]
        [String[]]
        $OrderBy,
        [parameter(Mandatory = $false)]
        [Alias('MaxResults')]
        [ValidateRange(1,1000)]
        [Int]
        $PageSize = 1000,
        [parameter(Mandatory = $false)]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Begin {
        if ($TeamDriveId) {
            $PSBoundParameters['Corpora'] = 'teamDrive'
            $PSBoundParameters['IncludeTeamDriveItems'] = $true
        }
        if ($ParentFolderId) {
            if ($Filter) {
                $Filter += "'$ParentFolderId' in parents"
                $PSBoundParameters['Filter'] += "'$ParentFolderId' in parents"
            }
            else {
                $Filter = @("'$ParentFolderId' in parents")
                $PSBoundParameters['Filter'] = @("'$ParentFolderId' in parents")
            }
        }
        if ($Fields -notcontains '*' -and $Fields -notcontains 'nextPageToken') {
            $Fields += 'nextPageToken'
        }
    }
    Process {
        Resolve-Email ([ref]$User)
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $request = $service.Files.List()
            $request.SupportsAllDrives = $true
            if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                $PageSize = $Limit
            }
            $request.PageSize = $PageSize
            if ($Fields) {
                $request.Fields = "$($Fields -join ",")"
            }
            foreach ($key in $PSBoundParameters.Keys | Where-Object { $_ -notin @('Fields','PageSize') }) {
                switch ($key) {
                    Filter {
                        $FilterFmt = ($PSBoundParameters[$key] -join " and ") -replace " -eq ","=" -replace " -like ",":" -replace " -match ",":" -replace " -contains ",":" -creplace "'True'","True" -creplace "'False'","False" -replace " -in "," in " -replace " -le ",'<=' -replace " -ge ",">=" -replace " -gt ",'>' -replace " -lt ",'<' -replace " -ne ","!=" -replace " -and "," and " -replace " -or "," or " -replace " -not "," not "
                        $request.Q = $FilterFmt
                    }
                    Spaces {
                        $request.$key = $($PSBoundParameters[$key] -join ",")
                    }
                    Default {
                        if ($request.PSObject.Properties.Name -contains $key) {
                            $request.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            $baseVerbose = "Getting"
            if ($Fields) {
                $baseVerbose += " Fields [$($Fields -join ",")] of"
            }
            $baseVerbose += " all Drive Files for User '$User'"
            if ($FilterFmt) {
                $baseVerbose += " matching Filter: $FilterFmt"
            }
            Write-Verbose $baseVerbose
            [int]$i = 1
            $overLimit = $false
            $originalLimit = $Limit
            do {
                $result = $request.Execute()
                $result.Files | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                [int]$retrieved = ($i + $result.Files.Count) - 1
                if ($Recurse -and ($Limit -eq 0 -or $retrieved -lt $Limit)) {
                    Write-Verbose "Starting recursive search..."
                    if ($Limit -gt 0) {
                        $Limit = $Limit - $result.Files.Count
                        Write-Verbose "[Prerecursion] Limit reduced to $Limit to account for $($result.Files.Count) files found in main search"
                    }
                    foreach ($subFolder in ($result.Files | Where-Object { $_.MimeType -eq 'application/vnd.google-apps.folder' } | Sort-Object Name)) {
                        Write-Verbose "Getting recursive file list of files under subfolder $($subFolder.Name) [$($subFolder.Id)]"
                        $params = @{
                            PageSize              = $PageSize
                            User                  = $User
                            ParentFolderId        = $subfolder.Id
                            IncludeTeamDriveItems = $IncludeTeamDriveItems
                            Recurse               = $true
                            Fields                = $Fields
                            Limit                 = $Limit
                            Verbose               = $false
                        }
                        Get-GSDriveFileList @params -OutVariable sub
                        Write-Verbose "Found $($sub.Count) files in subfolder $($subfolder.Name) [$($subfolder.Id)]"
                        [int]$retrieved += $sub.Count
                        if ($originalLimit -gt 0) {
                            $Limit = $originalLimit - $retrieved
                            Write-Verbose "[Postrecursion] Limit reduced to $Limit"
                            if ($retrieved -ge $originalLimit) {
                                break
                            }
                        }
                    }
                }
                if ($result.NextPageToken) {
                    $request.PageToken = $result.NextPageToken
                }
                Write-Verbose "Retrieved $retrieved Files..."
                if ($originalLimit -gt 0 -and $retrieved -ge $originalLimit) {
                    Write-Verbose "Limit reached: $originalLimit"
                    $overLimit = $true
                }
                elseif ($originalLimit -gt 0 -and ($retrieved + $PageSize) -gt $originalLimit) {
                    $newPS = $originalLimit - $retrieved
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                    $request.PageSize = $newPS
                }
                [int]$i = $i + $result.Files.Count
            }
            until ($overLimit -or !$result.NextPageToken)
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDriveFileList'
function Get-GSDriveFileUploadStatus {
    <#
    .SYNOPSIS
    Gets the current Drive file upload status
 
    .DESCRIPTION
    Gets the current Drive file upload status
 
    .PARAMETER Id
    The upload Id for the task you'd like to retrieve the status of
 
    .PARAMETER InProgress
    If passed, only returns upload statuses that are not 'Failed' or 'Completed'. If nothing is returned when passing this parameter, all tracked uploads have stopped
 
    .EXAMPLE
    Get-GSDriveFileUploadStatus -InProgress
 
    Gets the upload status for all tasks currently in progress
    #>

    [CmdletBinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Int[]]
        $Id,
        [parameter(Mandatory = $false)]
        [Switch]
        $InProgress
    )
    Begin {
        Write-Verbose "Getting Drive File Upload status"
    }
    Process {
        if ($script:DriveUploadTasks) {
            foreach ($task in $script:DriveUploadTasks) {
                $elapsed = ((Get-Date) - $task.StartTime)
                $progress = {$task.Request.GetProgress()}.InvokeReturnAsIs()
                if (-not $task.StreamDisposed -and $progress.Status -eq 'Completed') {
                    try {
                        Write-Verbose "[$($progress.Status)] Disposing stream of Task Id [$($task.Id)] | File [$($task.File.FullName)]"
                        $task.Stream.Dispose()
                        $task.StreamDisposed = $true
                    }
                    catch {
                        Write-Error $_
                    }
                }
                $bytesSent = $progress.BytesSent
                $remaining = try {
                    New-TimeSpan -Seconds $(($elapsed.TotalSeconds / ($bytesSent / ($task.Length))) - $elapsed.TotalSeconds) -ErrorAction Stop
                }
                catch {
                    New-TimeSpan
                }
                $percentComplete = if ($bytesSent) {
                    [Math]::Round((($bytesSent / $task.Length) * 100),4)
                }
                else {
                    0
                }
                if (-not $InProgress -or $progress.Status -notin @('Failed','Completed')) {
                    if ($Id) {
                        if ($Id -contains $task.Id) {
                            [PSCustomObject]@{
                                Id              = $task.Id
                                Status          = $progress.Status
                                PercentComplete = $percentComplete
                                Remaining       = $remaining
                                StartTime       = $task.StartTime
                                Elapsed         = $elapsed
                                File            = $task.File.FullName
                                Length          = $task.Length
                                Parents         = $task.Parents
                                BytesSent       = $bytesSent
                                FileLocked      = $(Test-FileLock -Path $task.File)
                                User            = $task.User
                                Exception       = $progress.Exception
                            }
                        }
                    }
                    else {
                        [PSCustomObject]@{
                            Id              = $task.Id
                            Status          = $progress.Status
                            PercentComplete = $percentComplete
                            Remaining       = $remaining
                            StartTime       = $task.StartTime
                            Elapsed         = $elapsed
                            File            = $task.File.FullName
                            Length          = $task.Length
                            Parents         = $task.Parents
                            BytesSent       = $bytesSent
                            FileLocked      = $(Test-FileLock -Path $task.File)
                            User            = $task.User
                            Exception       = $progress.Exception
                        }
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDriveFileUploadStatus'
function Get-GSDriveFolderSize {
    <#
    .SYNOPSIS
    Gets the size of the files with the specified ParentFolderId in Drive.
 
    .DESCRIPTION
    Gets the size of the files with the specified ParentFolderId in Drive.
 
    .PARAMETER ParentFolderId
    ID of parent folder to search to add to the filter
 
    .PARAMETER Recurse
    If True, recurses through subfolders found underneath primary search results
 
    .PARAMETER Depth
    Internal use only. Used to track how deep in the subfolder structure the command is currently searching when used with -Verbose
 
    .EXAMPLE
    Get-GSDriveFolderSize -ParentFolderId $id1,$id2 -Recurse
    #>


    [CmdletBinding()]
    Param (
        [parameter(Mandatory,Position = 0,ValueFromPipelineByPropertyName)]
        [Alias('Id')]
        [String[]]
        $ParentFolderId,
        [parameter()]
        [Switch]
        $Recurse,
        [parameter()]
        [Int]
        $Depth = 0
    )
    Begin {
        $final = @{
            TotalSize = 0
        }
    }
    Process {
        foreach ($id in $ParentFolderId) {
            $files = Get-GSDriveFileList -ParentFolderId $id -IncludeTeamDriveItems -Fields "files(name, id, size, mimeType)" -Verbose:$false
            $folderTotal = ($files.Size | Measure-Object -Sum).Sum
            if ($folderTotal){
                Write-Verbose ("Total file size in bytes in folder ID '$id': {0}" -f $folderTotal)
                $final.TotalSize +=  $folderTotal
            }
            if ($Recurse -and ($subfolders = $files | Where-Object {$_.MimeType -eq 'application/vnd.google-apps.folder'})) {
                $newDepth = $Depth + 1
                Write-Verbose "[Depth: $Depth > $newDepth] Recursively searching subfolder Ids: [ $($subfolders.Id -join ", ") ]"
                $subFolderTotal = Get-GSDriveFolderSize -ParentFolderId $subfolders.Id -Recurse -Depth $newDepth
                if ($subFolderTotal) {
                    $final.TotalSize += $subFolderTotal.TotalSize
                }
            }
        }
    }
    End {
        $final['TotalSizeInMB'] = $final['TotalSize'] / 1MB
        $final['TotalSizeInGB'] = $final['TotalSize'] / 1GB
        [PSCustomObject]$final
    }
}

Export-ModuleMember -Function 'Get-GSDriveFolderSize'
function Get-GSDrivePermission {
    <#
    .SYNOPSIS
    Gets permission information for a Drive file
 
    .DESCRIPTION
    Gets permission information for a Drive file
 
    .PARAMETER User
    The email or unique Id of the user whose Drive file permission you are trying to get
 
    Defaults to the AdminEmail user
 
    .PARAMETER FileId
    The unique Id of the Drive file
 
    .PARAMETER PermissionId
    The unique Id of the permission you are trying to get. If excluded, the list of permissions for the Drive file will be returned instead
 
    .PARAMETER PageSize
    The page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSDrivePermission -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976'
 
    Gets the list of permissions for the file Id
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Permission')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String]
        $FileId,
        [parameter(Mandatory = $false,Position = 1,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ParameterSetName = "Get")]
        [String[]]
        $PermissionId,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('MaxResults')]
        [ValidateRange(1,100)]
        [Int]
        $PageSize = 100,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($PermissionId) {
                foreach ($per in $PermissionId) {
                    $request = $service.Permissions.Get($FileId,$per)
                    $request.SupportsAllDrives = $true
                    $request.Fields = "*"
                    Write-Verbose "Getting Permission Id '$per' on File '$FileId' for user '$User'"
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru | Add-Member -MemberType NoteProperty -Name 'FileId' -Value $FileId -PassThru
                }
            }
            else {
                $request = $service.Permissions.List($FileId)
                $request.SupportsAllDrives = $true
                if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                    $PageSize = $Limit
                }
                $request.PageSize = $PageSize
                $request.Fields = "*"
                Write-Verbose "Getting Permission list on File '$FileId' for user '$User'"
                [int]$i = 1
                $overLimit = $false
                do {
                    $result = $request.Execute()
                    $result.Permissions | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru | Add-Member -MemberType NoteProperty -Name 'FileId' -Value $FileId -PassThru
                    if ($result.NextPageToken) {
                        $request.PageToken = $result.NextPageToken
                    }
                    [int]$retrieved = ($i + $result.Permissions.Count) - 1
                    Write-Verbose "Retrieved $retrieved Permissions..."
                    if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                        Write-Verbose "Limit reached: $Limit"
                        $overLimit = $true
                    }
                    elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                        $newPS = $Limit - $retrieved
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                        $request.PageSize = $newPS
                    }
                    [int]$i = $i + $result.Permissions.Count
                }
                until (!$result.NextPageToken)
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDrivePermission'
function Get-GSDriveProfile {
    <#
    .SYNOPSIS
    Gets Drive profile for the user
 
    .DESCRIPTION
    Gets Drive profile for the user
 
    .PARAMETER User
    The user to get profile of
 
    Defaults to the AdminEmail user
 
    .PARAMETER Fields
    The specific fields to request
 
    .EXAMPLE
    Get-GSDriveProfile
 
    Gets the Drive profile of the AdminEmail user
    #>

    [OutputType('Google.Apis.Drive.v3.Data.About')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,Position = 1)]
        [ValidateSet('AppInstalled','ExportFormats','FolderColorPalette','ImportFormats','Kind','MaxImportSizes','MaxUploadSize','StorageQuota','TeamDriveThemes','User')]
        [string[]]
        $Fields = @('AppInstalled','ExportFormats','FolderColorPalette','ImportFormats','Kind','MaxImportSizes','MaxUploadSize','StorageQuota','TeamDriveThemes','User')
    )
    Begin {
        $fieldDict = @{
            AppInstalled = 'appInstalled'
            ExportFormats = 'exportFormats'
            FolderColorPalette = 'folderColorPalette'
            ImportFormats = 'importFormats'
            Kind = 'kind'
            MaxImportSizes = 'maxImportSizes'
            MaxUploadSize = 'maxUploadSize'
            StorageQuota = 'storageQuota'
            TeamDriveThemes = 'teamDriveThemes'
            User = 'user'
        }
    }
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/drive'
                ServiceType = 'Google.Apis.Drive.v3.DriveService'
                User        = $U
            }
            $service = New-GoogleService @serviceParams
            try {
                $request = $service.About.Get()
                $request.Fields = "$(($Fields | ForEach-Object {$fieldDict[$_]}) -join ",")"
                Write-Verbose "Getting Drive profile for user '$U'"
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDriveProfile'
function Get-GSDriveRevision {
    <#
    .SYNOPSIS
    Gets information about a Drive file's revisions
 
    .DESCRIPTION
    Gets information about a Drive file's revisions
 
    .PARAMETER FileId
    The unique Id of the file to get revisions of
 
    .PARAMETER RevisionId
    The unique Id of the revision to get. If excluded, gets the list of revisions for the file
 
    .PARAMETER OutFilePath
    The path to save the revision to
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .PARAMETER Fields
    The specific fields to be returned
 
    .PARAMETER Force
    If $true, overwrites any existing files at the desired path
 
    .PARAMETER PageSize
    The maximum size of each page to return
 
    .PARAMETER Limit
    The maximum amount of objects to return
 
    .EXAMPLE
    Get-GSDriveFile -FileId $fileId | Get-GSDriveRevision
 
    Gets the list of revisions for the file
 
    .EXAMPLE
    Get-GSDriveRevision -FileId $fileId -Limit 1
 
    Gets the most recent revision for the file
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Revision')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String]
        $FileId,
        [parameter(Mandatory = $false,Position = 1,ParameterSetName = "Get")]
        [String[]]
        $RevisionId,
        [parameter(Mandatory = $false,ParameterSetName = "Get")]
        [Alias('SaveFileTo')]
        [String]
        $OutFilePath,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [String[]]
        $Fields = '*',
        [parameter(Mandatory = $false,ParameterSetName = "Get")]
        [Switch]
        $Force,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('MaxResults')]
        [ValidateRange(1,1000)]
        [Int]
        $PageSize = 1000,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                try {
                    foreach ($revision in $RevisionId) {
                        $request = $service.Revisions.Get($FileId,$revision)
                        $baseVerbose = "Getting"
                        if ($Fields) {
                            $baseVerbose += " Fields [$($Fields -join ",")] of"
                            $request.Fields = $($Fields -join ",")
                        }
                        $baseVerbose += " Drive File Revision Id '$revision' of File Id '$FileId' for User '$User'"
                        Write-Verbose $baseVerbose
                        $res = $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru | Add-Member -MemberType NoteProperty -Name 'FileId' -Value $FileId -PassThru
                        if ($OutFilePath) {
                            if (Test-Path $OutFilePath) {
                                $outFilePathItem = Get-Item $OutFilePath -ErrorAction SilentlyContinue
                                if ($outFilePathItem.PSIsContainer) {
                                    $resPath = $outFilePathItem.FullName
                                    $cleanedName = Get-SafeFileName "$($res.Name).$($res.FileExtension)"
                                    $filePath = Join-Path $resPath $cleanedName
                                }
                                elseif ($Force) {
                                    $endings = [System.Collections.Generic.List[string]]@('.bak')
                                    1..100 | ForEach-Object {$endings.Add(".bak$_")}
                                    foreach ($end in $endings) {
                                        $backupPath = "$($outFilePathItem.FullName)$end"
                                        if (-not (Test-Path $backupPath)) {
                                            break
                                        }
                                        else {
                                            $backupPath = $null
                                        }
                                    }
                                    Write-Warning "Renaming '$($outFilePathItem.Name)' to '$($outFilePathItem.Name).bak' in case replacement download fails."
                                    Rename-Item $outFilePathItem.FullName -NewName $backupPath -Force
                                    $filePath = $OutFilePath
                                }
                                else {
                                    throw "File already exists at path '$($OutFilePath)'. Please specify -Force to overwrite any files with the same name if they exist."
                                }
                            }
                            else {
                                $filePath = $OutFilePath
                            }
                            Write-Verbose "Saving file to path '$filePath'"
                            $stream = [System.IO.File]::Create($filePath)
                            $request.Download($stream)
                            $stream.Close()
                            $res | Add-Member -MemberType NoteProperty -Name OutFilePath -Value $filePath -Force
                            if ($backupPath) {
                                Write-Verbose "File has been downloaded successfully! Removing the backup file at path: $backupPath"
                                Remove-Item $backupPath -Recurse -Force -Confirm:$false
                            }
                        }
                        $res
                    }
                }
                catch {
                    $err = $_
                    if ($backupPath) {
                        if (Test-Path $outFilePathItem.FullName) {
                            Remove-Item $outFilePathItem.FullName -Recurse -Force
                        }
                        Rename-Item $backupPath -NewName $outFilePathItem.FullName -Force
                    }
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($err)
                    }
                    else {
                        Write-Error $err
                    }
                }
            }
            List {
                try {
                    $request = $service.Revisions.List($FileId)
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.PageSize = $PageSize
                    if ($Fields) {
                        $request.Fields = "$($Fields -join ",")"
                    }
                    $baseVerbose = "Getting"
                    if ($Fields) {
                        $baseVerbose += " Fields [$($Fields -join ",")] of"
                    }
                    $baseVerbose += " all Drive File Revisions of File Id '$FileId' for User '$User'"
                    Write-Verbose $baseVerbose
                    [int]$i = 1
                    $overLimit = $false
                    do {
                        $result = $request.Execute()
                        $result.Revisions | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru | Add-Member -MemberType NoteProperty -Name 'FileId' -Value $FileId -PassThru
                        if ($result.NextPageToken) {
                            $request.PageToken = $result.NextPageToken
                        }
                        [int]$retrieved = ($i + $result.Revisions.Count) - 1
                        Write-Verbose "Retrieved $retrieved Revisions..."
                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                            Write-Verbose "Limit reached: $Limit"
                            $overLimit = $true
                        }
                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                            $newPS = $Limit - $retrieved
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                            $request.PageSize = $newPS
                        }
                        [int]$i = $i + $result.Revisions.Count
                    }
                    until ($overLimit -or !$result.NextPageToken)
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSDriveRevision'
function Hide-GSDrive {
    <#
    .SYNOPSIS
    Hides a Shared Drive from the default view
 
    .DESCRIPTION
    Hides a Shared Drive from the default view
 
    .PARAMETER DriveId
    The unique Id of the Shared Drive to hide
 
    .PARAMETER User
    The email or unique Id of the user who you'd like to hide the Shared Drive for.
 
    Defaults to the AdminEmail user.
 
    .EXAMPLE
    Hide-GSDrive -DriveId $driveIds
 
    Hides the specified DriveIds for the AdminEmail user
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Drive')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Get")]
        [Alias('Id','TeamDriveId')]
        [String[]]
        $DriveId,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($id in $DriveId) {
            try {
                $request = $service.Drives.Hide($id)
                Write-Verbose "Hiding Shared Drive '$id' for user '$User'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Hide-GSDrive'
function New-GSDrive {
    <#
    .SYNOPSIS
    Creates a new Shared Drive
 
    .DESCRIPTION
    Creates a new Shared Drive
 
    .PARAMETER Name
    The name of the Shared Drive
 
    .PARAMETER User
    The user to create the Shared Drive for (must have permissions to create Shared Drives)
 
    .PARAMETER RequestId
    An ID, such as a random UUID, which uniquely identifies this user's request for idempotent creation of a Shared Drive. A repeated request by the same user and with the same request ID will avoid creating duplicates by attempting to create the same Shared Drive. If the Shared Drive already exists a 409 error will be returned.
 
    .PARAMETER CanAddChildren
    Whether the current user can add children to folders in this Shared Drive
 
    .PARAMETER CanChangeDriveBackground
    Whether the current user can change the background of this Shared Drive
 
    .PARAMETER CanComment
    Whether the current user can comment on files in this Shared Drive
 
    .PARAMETER CanCopy
    Whether the current user can copy files in this Shared Drive
 
    .PARAMETER CanDeleteDrive
    Whether the current user can delete this Shared Drive. Attempting to delete the Shared Drive may still fail if there are untrashed items inside the Shared Drive
 
    .PARAMETER CanDownload
    Whether the current user can download files in this Shared Drive
 
    .PARAMETER CanEdit
    Whether the current user can edit files in this Shared Drive
 
    .PARAMETER CanListChildren
    Whether the current user can list the children of folders in this Shared Drive
 
    .PARAMETER CanManageMembers
    Whether the current user can add members to this Shared Drive or remove them or change their role
 
    .PARAMETER CanReadRevisions
    Whether the current user can read the revisions resource of files in this Shared Drive
 
    .PARAMETER CanRemoveChildren
    Whether the current user can remove children from folders in this Shared Drive
 
    .PARAMETER CanRename
    Whether the current user can rename files or folders in this Shared Drive
 
    .PARAMETER CanRenameDrive
    Whether the current user can rename this Shared Drive
 
    .PARAMETER CanShare
    Whether the current user can share files or folders in this Shared Drive
 
    .EXAMPLE
    New-GSDrive -Name "Training Docs"
 
    Creates a new Shared Drive named "Training Docs"
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Drive')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $Name,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $RequestId = (New-Guid).ToString('N'),
        [parameter(Mandatory = $false)]
        [Switch]
        $CanAddChildren,
        [parameter(Mandatory = $false)]
        [Alias('CanChangeTeamDriveBackground')]
        [Switch]
        $CanChangeDriveBackground,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanComment,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanCopy,
        [parameter(Mandatory = $false)]
        [Alias('CanDeleteTeamDrive')]
        [Switch]
        $CanDeleteDrive,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanDownload,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanEdit,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanListChildren,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanManageMembers,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanReadRevisions,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanRemoveChildren,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanRename,
        [parameter(Mandatory = $false)]
        [Alias('CanRenameTeamDrive')]
        [Switch]
        $CanRenameDrive,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanShare
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Drive.v3.Data.Drive'
            $capabilities = New-Object 'Google.Apis.Drive.v3.Data.Drive+CapabilitiesData'
            foreach ($key in $PSBoundParameters.Keys) {
                switch ($key) {
                    Default {
                        if ($capabilities.PSObject.Properties.Name -contains $key) {
                            $capabilities.$key = $PSBoundParameters[$key]
                        }
                        elseif ($body.PSObject.Properties.Name -contains $key) {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            $body.Capabilities = $capabilities
            $request = $service.Drives.Create($body,$RequestId)
            Write-Verbose "Creating Shared Drive '$Name' for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru | Add-Member -MemberType NoteProperty -Name 'RequestId' -Value $RequestId -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSDrive'
function New-GSDriveFile {
    <#
    .SYNOPSIS
    Creates a blank Drive file
 
    .DESCRIPTION
    Creates a blank Drive file
 
    .PARAMETER User
    The email or unique Id of the user who you are creating the Drive file for
 
    Defaults to the AdminEmail user
 
    .PARAMETER Name
    The name of the new Drive file
 
    .PARAMETER Description
    The description of the Drive file
 
    .PARAMETER FolderColorRgb
    The color for a folder as an RGB hex string.
 
    Available values are:
    * "ChocolateIceCream"
    * "OldBrickRed"
    * "Cardinal"
    * "WildStrawberries"
    * "MarsOrange"
    * "YellowCab"
    * "Spearmint"
    * "VernFern"
    * "Asparagus"
    * "SlimeGreen"
    * "DesertSand"
    * "Macaroni"
    * "SeaFoam"
    * "Pool"
    * "Denim"
    * "RainySky"
    * "BlueVelvet"
    * "PurpleDino"
    * "Mouse"
    * "MountainGrey"
    * "Earthworm"
    * "BubbleGum"
    * "PurpleRain"
    * "ToyEggplant"
 
    .PARAMETER Parents
    The parent folder Id of the new Drive file
 
    .PARAMETER MimeType
    The Google Mime Type of the new Drive file
 
    Available values are:
    * "Audio"
    * "Docs"
    * "Drawing"
    * "DriveFile"
    * "DriveFolder"
    * "Form"
    * "FusionTables"
    * "Map"
    * "Photo"
    * "Slides"
    * "AppsScript"
    * "Sites"
    * "Sheets"
    * "Unknown"
    * "Video"
 
    .PARAMETER CustomMimeType
    The custom Mime Type of the new Drive file
 
    .PARAMETER Projection
    The defined subset of fields to be returned
 
    Available values are:
    * "Minimal"
    * "Standard"
    * "Full"
    * "Access"
 
    .PARAMETER Fields
    The specific fields to returned
 
    .EXAMPLE
    New-GSDriveFile -Name "Training Docs" -MimeType DriveFolder
 
    Creates a new folder in Drive named "Training Docs" in the root OrgUnit for the AdminEmail user
    #>

    [OutputType('Google.Apis.Drive.v3.Data.File')]
    [cmdletbinding(DefaultParameterSetName = "BuiltIn")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $true)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [ValidateSet('ChocolateIceCream','OldBrickRed','Cardinal','WildStrawberries','MarsOrange','YellowCab','Spearmint','VernFern','Asparagus','SlimeGreen','DesertSand','Macaroni','SeaFoam','Pool','Denim','RainySky','BlueVelvet','PurpleDino','Mouse','MountainGrey','Earthworm','BubbleGum','PurpleRain','ToyEggplant')]
        [string]
        $FolderColorRgb,
        [parameter(Mandatory = $false)]
        [Alias('ParentId')]
        [String[]]
        $Parents,
        [parameter(Mandatory = $true,ParameterSetName = "BuiltIn")]
        [Alias('Type')]
        [ValidateSet("Audio","Docs","Drawing","DriveFile","DriveFolder","Form","FusionTables","Map","Photo","Slides","AppsScript","Sites","Sheets","Unknown","Video")]
        [String]
        $MimeType,
        [parameter(Mandatory = $true,ParameterSetName = "Custom")]
        [String]
        $CustomMimeType,
        [parameter(Mandatory = $false)]
        [Alias('Depth')]
        [ValidateSet("Minimal","Standard","Full","Access")]
        [String]
        $Projection = "Full",
        [parameter(Mandatory = $false)]
        [ValidateSet("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","thumbnailLink","thumbnailVersion","trashed","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")]
        [String[]]
        $Fields
    )
    Begin {
        $colorDictionary = @{
            ChocolateIceCream = '#ac725e'
            OldBrickRed       = '#d06b64'
            Cardinal          = '#f83a22'
            WildStrawberries  = '#fa573c'
            MarsOrange        = '#ff7537'
            YellowCab         = '#ffad46'
            Spearmint         = '#42d692'
            VernFern          = '#16a765'
            Asparagus         = '#7bd148'
            SlimeGreen        = '#b3dc6c'
            DesertSand        = '#fbe983'
            Macaroni          = '#fad165'
            SeaFoam           = '#92e1c0'
            Pool              = '#9fe1e7'
            Denim             = '#9fc6e7'
            RainySky          = '#4986e7'
            BlueVelvet        = '#9a9cff'
            PurpleDino        = '#b99aff'
            Mouse             = '#8f8f8f'
            MountainGrey      = '#cabdbf'
            Earthworm         = '#cca6ac'
            BubbleGum         = '#f691b2'
            PurpleRain        = '#cd74e6'
            ToyEggplant       = '#a47ae2'
        }
        $mimeHash = @{
            Audio        = "application/vnd.google-apps.audio"
            Docs         = "application/vnd.google-apps.document"
            Drawing      = "application/vnd.google-apps.drawing"
            DriveFile    = "application/vnd.google-apps.file"
            DriveFolder  = "application/vnd.google-apps.folder"
            Form         = "application/vnd.google-apps.form"
            FusionTables = "application/vnd.google-apps.fusiontable"
            Map          = "application/vnd.google-apps.map"
            Photo        = "application/vnd.google-apps.photo"
            Slides       = "application/vnd.google-apps.presentation"
            AppsScript   = "application/vnd.google-apps.script"
            Sites        = "application/vnd.google-apps.sites"
            Sheets       = "application/vnd.google-apps.spreadsheet"
            Unknown      = "application/vnd.google-apps.unknown"
            Video        = "application/vnd.google-apps.video"
        }
        if ($Projection) {
            $fs = switch ($Projection) {
                Standard {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","owners","parents","properties","version","webContentLink","webViewLink")
                }
                Access {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","ownedByMe","owners","parents","permissionIds","permissions","shared","sharedWithMeTime","sharingUser","viewedByMe","viewedByMeTime","viewersCanCopyContent","writersCanShare")
                }
                Full {
                    @("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasAugmentedPermissions","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissionIds","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","teamDriveId","thumbnailLink","thumbnailVersion","trashed","trashedTime","trashingUser","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")
                }
            }
        }
        elseif ($PSBoundParameters.ContainsKey('Fields')) {
            $fs = $Fields
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Drive.v3.Data.File'
            if ($MimeType) {
                $body.MimeType = $mimeHash[$MimeType]
            }
            elseif ($CustomMimeType) {
                $body.MimeType = $CustomMimeType
            }
            if ($Description) {
                $body.Description = $Description
            }
            if ($FolderColorRgb) {
                $body.FolderColorRgb = $ColorDictionary[$FolderColorRgb]
            }
            if ($Name) {
                $body.Name = [String]$Name
            }
            if ($Parents) {
                $body.Parents = [String[]]$Parents
            }
            $request = $service.Files.Create($body)
            $request.SupportsAllDrives = $true
            if ($fs) {
                $request.Fields = $($fs -join ",")
            }
            Write-Verbose "Creating file '$Name' for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSDriveFile'
function Remove-GSDrive {
    <#
    .SYNOPSIS
    Removes a Shared Drive
 
    .DESCRIPTION
    Removes a Shared Drive
 
    .PARAMETER DriveId
    The Id of the Shared Drive to remove
 
    .PARAMETER User
    The email or unique Id of the user with permission to delete the Shared Drive
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Remove-Drive -DriveId "0AJ8Xjq3FcdCKUk9PVA" -Confirm:$false
 
    Removes the Shared Drive '0AJ8Xjq3FcdCKUk9PVA', skipping confirmation
    #>

    [cmdletbinding(SupportsShouldProcess=$true,ConfirmImpact="High")]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id','TeamDriveId')]
        [String[]]
        $DriveId,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            foreach ($id in $DriveId) {
                if ($PSCmdlet.ShouldProcess("Deleting Shared Drive '$id' from user '$User'")) {
                    Write-Verbose "Deleting Shared Drive '$id' from user '$User'"
                    $request = $service.Drives.Delete($id)
                    $request.Execute()
                    Write-Verbose "Shared Drive '$id' successfully deleted from user '$User'"
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSDrive'
function Remove-GSDriveFile {
    <#
    .SYNOPSIS
    Deletes a Drive file
 
    .DESCRIPTION
    Deletes a Drive file
 
    .PARAMETER FileId
    The unique Id(s) of the file(s) to delete
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Remove-GSDriveFile -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -User user@domain.com
 
    Deletes the file with ID 1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976 from the user user@domain.com's Drive
 
    .EXAMPLE
    Get-GSDriveFileList -ParentFolderId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -User user@domain.com | Remove-GSDriveFile
 
    Get the file with ID 1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976 from the user user@domain.com's Drive and pipeline
    #>

    [cmdletbinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    Param(
        [parameter(Mandatory,ValueFromPipelineByPropertyName,Position = 0)]
        [Alias('Id')]
        [String[]]
        $FileId,
        [parameter(ValueFromPipelineByPropertyName,Position = 1)]
        [Alias('Owner', 'PrimaryEmail', 'UserKey', 'Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        Resolve-Email ([ref]$User)
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($file in $FileId) {
            try {
                if ($PSCmdlet.ShouldProcess("Deleting File Id '$FileId' from user '$User'")) {
                    Write-Verbose "Deleting File Id '$FileId' from user '$User'"
                    $request = $service.Files.Delete($FileId)
                    $request.SupportsAllDrives = $true
                    $request.Execute() | Out-Null
                    Write-Verbose "File Id '$FileId' successfully deleted from user '$User'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSDriveFile'
function Remove-GSDrivePermission {
    <#
    .SYNOPSIS
    Removes a permission from a Drive file
 
    .DESCRIPTION
    Removes a permission from a Drive file
 
    .PARAMETER User
    The email or unique Id of the user whose Drive file permission you are trying to get
 
    Defaults to the AdminEmail user
 
    .PARAMETER FileId
    The unique Id of the Drive file
 
    .PARAMETER PermissionId
    The unique Id of the permission you are trying to remove.
 
    .EXAMPLE
    Remove-GSDrivePermission -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -PermissionID 'sdfadsfsdafasd'
 
    Removes the permission from the drive.
 
    .EXAMPLE
    Get-GSDrivePermission -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' | ? {$_.Type -eq 'group'} | Remove-GSDrivePermission
 
    Gets the permissions assigned to groups and removes them.
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [String]
        $FileId,
        [parameter(Mandatory = $false,Position = 1,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String]
        $PermissionId
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($PSCmdlet.ShouldProcess("Removing Drive Permission Id '$PermissionId' from FileId '$FileID'")) {
                $request = $service.Permissions.Delete($FileId,$PermissionId)
                $request.SupportsAllDrives = $true
                $request.Execute()
                Write-Verbose "Successfully removed Drive Permission Id '$PermissionId' from FileId '$FileID'"
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSDrivePermission'
function Remove-GSDriveRevision {
    <#
    .SYNOPSIS
    Permanently deletes a file version.
 
    .DESCRIPTION
    Permanently deletes a file version.
 
    You can only delete revisions for files with binary content in Google Drive, like images or videos. Revisions for other files, like Google Docs or Sheets, and the last remaining file version can't be deleted.
 
    .PARAMETER FileId
    The unique Id of the file to remove revisions from
 
    .PARAMETER RevisionId
    The unique Id of the revision to remove
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSDriveRevision -FileId $fileId -Limit 1 | Remove-GSDriveRevision
 
    Removes the oldest revision for the file
 
    .EXAMPLE
    Get-GSDriveRevision -FileId $fileId | Select-Object -Last 1 | Remove-GSDriveRevision
 
    Removes the newest revision for the file
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [String]
        $FileId,
        [parameter(Mandatory = $true,Position = 1,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $RevisionId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($id in $RevisionId) {
            try {
                if ($PSCmdlet.ShouldProcess("Removing Drive Revision Id '$id' from FileId '$FileID' for user '$User'")) {
                    $request = $service.Revisions.Delete($FileId,$id)
                    $request.Execute()
                    Write-Verbose "Successfully removed Drive Revision Id '$id' from FileId '$FileID' for user '$User'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSDriveRevision'
function Set-GSDocContent {
    <#
    .SYNOPSIS
    Sets the content of a Google Doc. This overwrites any existing content on the Doc
 
    .DESCRIPTION
    Sets the content of a Google Doc. This overwrites any existing content on the Doc
 
    .PARAMETER FileID
    The unique Id of the file to set content on
 
    .PARAMETER Value
    The content to set
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    $logStrings | Set-GSDocContent -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976'
 
    Sets the content of the specified Google Doc to the strings in the $logStrings variable. Any existing content on the doc will be overwritten.
    #>

    [CmdLetBinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $FileID,
        [parameter(Mandatory = $true,ValueFromPipeline = $true)]
        [String[]]
        $Value,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $service = New-GoogleService @serviceParams
        $stream = New-Object 'System.IO.MemoryStream'
        $writer = New-Object 'System.IO.StreamWriter' $stream
        $concatStrings = @()
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        foreach ($string in $Value) {
            $concatStrings += $string
        }
    }
    End {
        try {
            $concatStrings = $concatStrings -join "`n"
            $writer.Write($concatStrings)
            $writer.Flush()
            $contentType = 'text/plain'
            $body = New-Object 'Google.Apis.Drive.v3.Data.File'
            $request = $service.Files.Update($body,$FileId,$stream,$contentType)
            $request.QuotaUser = $User
            $request.ChunkSize = 512KB
            $request.SupportsAllDrives = $true
            Write-Verbose "Setting content for File '$FileID'"
            $request.Upload() | Out-Null
            $stream.Close()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Set-GSDocContent'
function Show-GSDrive {
    <#
    .SYNOPSIS
    Shows (unhides) a Shared Drive in the default view
 
    .DESCRIPTION
    Shows (unhides) a Shared Drive in the default view
 
    .PARAMETER DriveId
    The unique Id of the Shared Drive to unhide
 
    .PARAMETER User
    The email or unique Id of the user who you'd like to unhide the Shared Drive for.
 
    Defaults to the AdminEmail user.
 
    .EXAMPLE
    Show-GSDrive -DriveId $driveIds
 
    Unhides the specified DriveIds for the AdminEmail user
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Drive')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Get")]
        [Alias('Id','TeamDriveId')]
        [String[]]
        $DriveId,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($id in $DriveId) {
            try {
                $request = $service.Drives.Unhide($id)
                Write-Verbose "Unhiding Shared Drive '$id' for user '$User'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Show-GSDrive'
function Start-GSDriveFileUpload {
    <#
    .SYNOPSIS
    Starts uploading a file or list of files to Drive asynchronously
 
    .DESCRIPTION
    Starts uploading a file or list of files to Drive asynchronously. Allows full folder structure uploads by passing a folder as -Path and including the -Recurse parameter
 
    .PARAMETER Path
    The path of the file or folder to upload
 
    .PARAMETER Description
    The description of the file or folder in Drive
 
    .PARAMETER Parents
    The unique Id of the parent folder in Drive to upload the file to
 
    Defaults to the root folder in My Drive
 
    .PARAMETER Recurse
    If $true and there is a Directory passed to -Path, this will rebuild the folder structure in Drive under the Parent Id and upload the files within accordingly
 
    .PARAMETER Wait
    If $true, waits for all uploads to complete and shows progress around the total upload
 
    .PARAMETER RetryCount
    How many times uploads should be retried when using the -Wait parameter
 
    Defaults to 10
 
    .PARAMETER ThrottleLimit
    The limit of files to upload per batch while waiting
 
    .PARAMETER User
    The email or unique Id of the user to upload the files for
 
    .EXAMPLE
    Start-GSDriveFileUpload -Path "C:\Scripts","C:\Modules" -Recurse -Wait
 
    Starts uploading the Scripts and Modules folders and the files within them and waits for the uploads to complete, showing progress as files are uploaded
    #>

    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [ValidateScript( {Test-Path $_})]
        [String[]]
        $Path,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [String[]]
        $Parents,
        [parameter(Mandatory = $false)]
        [Switch]
        $Recurse,
        [parameter(Mandatory = $false)]
        [Switch]
        $Wait,
        [parameter(Mandatory = $false)]
        [Int]
        $RetryCount = 10,
        [parameter(Mandatory = $false)]
        [ValidateRange(1,1000)]
        [Int]
        $ThrottleLimit = 20,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $taskList = [System.Collections.ArrayList]@()
        $fullTaskList = [System.Collections.ArrayList]@()
        $start = Get-Date
        $folIdHash = @{}
        $throttleCount = 0
        $totalThrottleCount = 0
        $totalFiles = 0
        $successParam = @{
            Successful = $false
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            foreach ($file in $Path) {
                $details = Get-Item $file
                if ($details.PSIsContainer) {
                    $newFolPerms = @{
                        Name    = $details.Name
                        User    = $User
                        Type    = 'DriveFolder'
                        Verbose = $false
                    }
                    $parentVerbose = $null
                    if ($PSBoundParameters.Keys -contains 'Parents') {
                        $newFolPerms['Parents'] = $PSBoundParameters['Parents']
                        if ($par = Get-GSDriveFileInfo -FileId $PSBoundParameters['Parents'] -User $User -Verbose:$false -ErrorAction Stop) {
                            $parentVerbose = " under parent folder '$($par.Name -join ',')'"
                        }
                    }
                    Write-Verbose "Creating new Drive folder '$($details.Name)'$parentVerbose"
                    $id = New-GSDriveFile @newFolPerms | Select-Object -ExpandProperty Id
                    $folIdHash[$details.FullName.TrimEnd('\').TrimEnd('/')] = $id
                    if ($Recurse) {
                        $recurseList = Get-ChildItem $details.FullName -Recurse
                        $recDirs = $recurseList | Where-Object {$_.PSIsContainer} | Sort-Object FullName
                        if ($recDirs) {
                            Write-Verbose "Creating recursive folder structure under '$($details.Name)'"
                            $recDirs | ForEach-Object {
                                $parPath = "$(Split-Path $_.FullName -Parent)"
                                $newFolPerms = @{
                                    Name    = $_.Name
                                    User    = $User
                                    Type    = 'DriveFolder'
                                    Parents = [String[]]$folIdHash[$parPath]
                                    Verbose = $false
                                }
                                $id = New-GSDriveFile @newFolPerms | Select-Object -ExpandProperty Id
                                $folIdHash[$_.FullName] = $id
                                Write-Verbose " + Created: [ID $id] $($_.FullName -replace "^$([RegEx]::Escape($details.FullName))",$details.Name)"
                            }
                        }
                        $details = $recurseList | Where-Object {!$_.PSIsContainer} | Sort-Object FullName
                        $checkFolIdHash = $true
                        $totalFiles = [int]$totalFiles + $details.Count
                    }
                }
                else {
                    $totalFiles++
                    $checkFolIdHash = $false
                }
                foreach ($detPart in $details) {
                    $throttleCount++
                    $contentType = Get-MimeType $detPart
                    $body = New-Object 'Google.Apis.Drive.v3.Data.File' -Property @{
                        Name = [String]$detPart.Name
                    }
                    if (!$checkFolIdHash -and ($PSBoundParameters.Keys -contains 'Parents')) {
                        if ($Parents) {
                            $body.Parents = [String[]]$Parents
                        }
                    }
                    elseif ($checkFolIdHash) {
                        $parPath = "$(Split-Path $detPart.FullName -Parent)"
                        $body.Parents = [String[]]$folIdHash[$parPath]
                    }
                    if ($Description) {
                        $body.Description = $Description
                    }
                    $stream = New-Object 'System.IO.FileStream' $detPart.FullName,([System.IO.FileMode]::Open),([System.IO.FileAccess]::Read),([System.IO.FileShare]"Delete, ReadWrite")
                    $request = $service.Files.Create($body,$stream,$contentType)
                    $request.QuotaUser = $User
                    $request.SupportsAllDrives = $true
                    $request.ChunkSize = 512KB
                    $upload = $request.UploadAsync()
                    $task = $upload.ContinueWith([System.Action[System.Threading.Tasks.Task]] {if ($stream) {
                                $stream.Dispose()
                            }})
                    Write-Verbose "[$($detPart.Name)] Upload Id $($upload.Id) has started"
                    if (!$Script:DriveUploadTasks) {
                        $Script:DriveUploadTasks = [System.Collections.ArrayList]@()
                    }
                    $script:DriveUploadTasks += [PSCustomObject]@{
                        Id             = $upload.Id
                        File           = $detPart
                        Length         = $detPart.Length
                        SizeInMB       = [Math]::Round(($detPart.Length / 1MB),2,[MidPointRounding]::AwayFromZero)
                        StartTime      = $(Get-Date)
                        Parents        = $body.Parents
                        User           = $User
                        Upload         = $upload
                        Request        = $request
                        Stream         = $stream
                        StreamDisposed = $false
                    }
                    $taskList += [PSCustomObject]@{
                        Id       = $upload.Id
                        File     = $detPart
                        SizeInMB = [Math]::Round(($detPart.Length / 1MB),2,[MidPointRounding]::AwayFromZero)
                        User     = $User
                    }
                    $fullTaskList += [PSCustomObject]@{
                        Id       = $upload.Id
                        File     = $detPart
                        SizeInMB = [Math]::Round(($detPart.Length / 1MB),2,[MidPointRounding]::AwayFromZero)
                        User     = $User
                    }
                    if ($throttleCount -ge $ThrottleLimit) {
                        $totalThrottleCount += $throttleCount
                        if ($Wait) {
                            Watch-GSDriveUpload -Id $taskList.Id -CountUploaded $totalThrottleCount -TotalUploading $totalFiles
                            $throttleCount = 0
                            $taskList = [System.Collections.ArrayList]@()
                        }
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
        finally {
            try {
                [System.Console]::CursorVisible = $true
            }
            catch {
                if ($Error[0].Exception.Message -eq 'Exception setting "CursorVisible": "The handle is invalid."') {
                    $Global:Error.Remove($Global:Error[0])
                }
            }
        }
    }
    End {
        if (-not $Wait) {
            $fullTaskList
        }
        else {
            try {
                Watch-GSDriveUpload -Id $fullTaskList.Id -CountUploaded $totalFiles -TotalUploading $totalFiles
                $fullStatusList = Get-GSDriveFileUploadStatus -Id $fullTaskList.Id
                $failedFiles = $fullStatusList | Where-Object {$_.Status -eq "Failed"}
                if (!$failedFiles) {
                    Write-Verbose "All files uploaded to Google Drive successfully! Total time: $("{0:c}" -f ((Get-Date) - $start) -replace "\..*")"
                    $successParam['Successful'] = $true
                }
                elseif ($RetryCount) {
                    $totalRetries = 0
                    do {
                        $throttleCount = 0
                        $totalThrottleCount = 0
                        $taskList = [System.Collections.ArrayList]@()
                        $fullTaskList = [System.Collections.ArrayList]@()
                        $details = Get-Item $failedFiles.File
                        $totalFiles = [int]$totalFiles + $details.Count
                        $totalRetries++
                        Write-Verbose "~ ~ ~ RETRYING [$totalFiles] FAILED FILES [Retry # $totalRetries / $RetryCount] ~ ~ ~"
                        $details = Get-Item $failedFiles.File
                        foreach ($detPart in $details) {
                            $throttleCount++
                            $contentType = Get-MimeType $detPart
                            $body = New-Object 'Google.Apis.Drive.v3.Data.File' -Property @{
                                Name = [String]$detPart.Name
                            }
                            $parPath = "$(Split-Path $detPart.FullName -Parent)"
                            $body.Parents = [String[]]$folIdHash[$parPath]
                            if ($Description) {
                                $body.Description = $Description
                            }
                            $stream = New-Object 'System.IO.FileStream' $detPart.FullName,([System.IO.FileMode]::Open),([System.IO.FileAccess]::Read),([System.IO.FileShare]::Delete + [System.IO.FileShare]::ReadWrite)
                            $request = $service.Files.Create($body,$stream,$contentType)
                            $request.QuotaUser = $User
                            $request.SupportsAllDrives = $true
                            $request.ChunkSize = 512KB
                            $upload = $request.UploadAsync()
                            $task = $upload.ContinueWith([System.Action[System.Threading.Tasks.Task]] {$stream.Dispose()})
                            Write-Verbose "[$($detPart.Name)] Upload Id $($upload.Id) has started"
                            if (!$Script:DriveUploadTasks) {
                                $Script:DriveUploadTasks = [System.Collections.ArrayList]@()
                            }
                            $script:DriveUploadTasks += [PSCustomObject]@{
                                Id             = $upload.Id
                                File           = $detPart
                                Length         = $detPart.Length
                                SizeInMB       = [Math]::Round(($detPart.Length / 1MB),2,[MidPointRounding]::AwayFromZero)
                                StartTime      = $(Get-Date)
                                Parents        = $body.Parents
                                User           = $User
                                Upload         = $upload
                                Request        = $request
                                Stream         = $stream
                                StreamDisposed = $false
                            }
                            $taskList += [PSCustomObject]@{
                                Id       = $upload.Id
                                File     = $detPart
                                SizeInMB = [Math]::Round(($detPart.Length / 1MB),2,[MidPointRounding]::AwayFromZero)
                                User     = $User
                            }
                            $fullTaskList += [PSCustomObject]@{
                                Id       = $upload.Id
                                File     = $detPart
                                SizeInMB = [Math]::Round(($detPart.Length / 1MB),2,[MidPointRounding]::AwayFromZero)
                                User     = $User
                            }
                            if ($throttleCount -ge $ThrottleLimit) {
                                $totalThrottleCount += $throttleCount
                                if ($Wait) {
                                    Watch-GSDriveUpload -Id $taskList.Id -CountUploaded $totalThrottleCount -TotalUploading $totalFiles -Action Retrying
                                    $throttleCount = 0
                                    $taskList = [System.Collections.ArrayList]@()
                                }
                            }
                        }
                        Watch-GSDriveUpload -Id $fullTaskList.Id -Action Retrying -CountUploaded $totalFiles -TotalUploading $totalFiles
                        $fullStatusList = Get-GSDriveFileUploadStatus -Id $fullTaskList.Id
                        $failedFiles = $fullStatusList | Where-Object {$_.Status -eq "Failed"}
                    }
                    until (!$failedFiles -or ($totalRetries -ge $RetryCount))
                    if ($failedFiles) {
                        Write-Warning "The following files failed to upload:`n`n$($failedFiles | Select-Object Id,Status,Exception,File | Format-List | Out-String)"
                    }
                    elseif (!$failedFiles) {
                        Write-Verbose "All files uploaded to Google Drive successfully! Total time: $("{0:c}" -f ((Get-Date) - $start) -replace "\..*")"
                        $successParam['Successful'] = $true
                    }
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
            finally {
                if ($Host.Name -and $Host.Name -notlike "Windows*PowerShell*ISE*") {
                    try {
                        [System.Console]::CursorVisible = $true
                    }
                    catch {
                        if ($Error[0].Exception.Message -eq 'Exception setting "CursorVisible": "The handle is invalid."') {
                            $Global:Error.Remove($Global:Error[0])
                        }
                    }
                }
                Stop-GSDriveFileUpload @successParam
            }
        }
    }
}

Export-ModuleMember -Function 'Start-GSDriveFileUpload'
function Stop-GSDriveFileUpload {
    <#
    .SYNOPSIS
    Stops all Drive file uploads in progress and disposes of all streams.
 
    .DESCRIPTION
    Stops all Drive file uploads in progress and disposes of all streams.
 
    .PARAMETER Successful
    If $true, hides any failed task verbose output
 
    .EXAMPLE
    Stop-GSDriveFileUpload
 
    Stops all Drive file uploads in progress and disposes of all streams.
    #>

    [cmdletbinding()]
    Param (
        [Parameter(DontShow)]
        [Switch]
        $Successful
    )
    Begin {
        Write-Verbose "Stopping all remaining Drive file uploads!"
    }
    Process {
        foreach ($task in $script:DriveUploadTasks | Where-Object {$_.Stream -and -not $_.StreamDisposed}) {
            try {
                if (-not $Successful) {
                    $progress = {$task.Request.GetProgress()}.InvokeReturnAsIs()
                    Write-Verbose "[$($progress.Status)] Stopping stream of Task Id [$($task.Id)] | File [$($task.File.FullName)]"
                }
                $task.Stream.Dispose()
                $task.StreamDisposed = $true
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Stop-GSDriveFileUpload'
function Update-GSDrive {
    <#
    .SYNOPSIS
    Update metatdata for a Shared Drive
 
    .DESCRIPTION
    Update metatdata for a Shared Drive
 
    .PARAMETER DriveId
    The unique Id of the Shared Drive to update
 
    .PARAMETER User
    The user to create the Shared Drive for (must have permissions to create Shared Drives)
 
    .PARAMETER Name
    The name of the Shared Drive
 
    .PARAMETER CanAddChildren
    Whether the current user can add children to folders in this Shared Drive
 
    .PARAMETER CanChangeDriveBackground
    Whether the current user can change the background of this Shared Drive
 
    .PARAMETER CanComment
    Whether the current user can comment on files in this Shared Drive
 
    .PARAMETER CanCopy
    Whether the current user can copy files in this Shared Drive
 
    .PARAMETER CanDeleteDrive
    Whether the current user can delete this Shared Drive. Attempting to delete the Shared Drive may still fail if there are untrashed items inside the Shared Drive
 
    .PARAMETER CanDownload
    Whether the current user can download files in this Shared Drive
 
    .PARAMETER CanEdit
    Whether the current user can edit files in this Shared Drive
 
    .PARAMETER CanListChildren
    Whether the current user can list the children of folders in this Shared Drive
 
    .PARAMETER CanManageMembers
    Whether the current user can add members to this Shared Drive or remove them or change their role
 
    .PARAMETER CanReadRevisions
    Whether the current user can read the revisions resource of files in this Shared Drive
 
    .PARAMETER CanRemoveChildren
    Whether the current user can remove children from folders in this Shared Drive
 
    .PARAMETER CanRename
    Whether the current user can rename files or folders in this Shared Drive
 
    .PARAMETER CanRenameDrive
    Whether the current user can rename this Shared Drive
 
    .PARAMETER CanShare
    Whether the current user can share files or folders in this Shared Drive
 
    .EXAMPLE
    Update-GSDrive -DriveId '0AJ8Xjq3FcdCKUk9PVA' -Name "HR Document Repo"
 
    Updated the Shared Drive with a new name, "HR Document Repo"
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Drive')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [Alias('Id','TeamDriveId')]
        [String]
        $DriveId,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanAddChildren,
        [parameter(Mandatory = $false)]
        [Alias('CanChangeTeamDriveBackground')]
        [Switch]
        $CanChangeDriveBackground,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanComment,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanCopy,
        [parameter(Mandatory = $false)]
        [Alias('CanDeleteTeamDrive')]
        [Switch]
        $CanDeleteDrive,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanDownload,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanEdit,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanListChildren,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanManageMembers,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanReadRevisions,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanRemoveChildren,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanRename,
        [parameter(Mandatory = $false)]
        [Alias('CanRenameTeamDrive')]
        [Switch]
        $CanRenameDrive,
        [parameter(Mandatory = $false)]
        [Switch]
        $CanShare
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Drive.v3.Data.Drive'
            $capabilities = New-Object 'Google.Apis.Drive.v3.Data.Drive+CapabilitiesData'
            foreach ($key in $PSBoundParameters.Keys) {
                switch ($key) {
                    Default {
                        if ($capabilities.PSObject.Properties.Name -contains $key) {
                            $capabilities.$key = $PSBoundParameters[$key]
                        }
                        elseif ($body.PSObject.Properties.Name -contains $key) {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            $body.Capabilities = $capabilities
            $request = $service.Drives.Update($body,$DriveId)
            Write-Verbose "Updating Shared Drive '$Name' for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSDrive'
function Update-GSDriveFile {
    <#
    .SYNOPSIS
    Updates the metadata for a Drive file
 
    .DESCRIPTION
    Updates the metadata for a Drive file
 
    .PARAMETER FileId
    The unique Id of the Drive file to Update
 
    .PARAMETER Path
    The path to the local file whose content you would like to upload to Drive.
 
    .PARAMETER Name
    The name of the Drive file
 
    .PARAMETER Description
    The description of the Drive file
 
    .PARAMETER FolderColorRgb
    The color for a folder as an RGB hex string.
 
    Available values are:
    * "ChocolateIceCream"
    * "OldBrickRed"
    * "Cardinal"
    * "WildStrawberries"
    * "MarsOrange"
    * "YellowCab"
    * "Spearmint"
    * "VernFern"
    * "Asparagus"
    * "SlimeGreen"
    * "DesertSand"
    * "Macaroni"
    * "SeaFoam"
    * "Pool"
    * "Denim"
    * "RainySky"
    * "BlueVelvet"
    * "PurpleDino"
    * "Mouse"
    * "MountainGrey"
    * "Earthworm"
    * "BubbleGum"
    * "PurpleRain"
    * "ToyEggplant"
 
    .PARAMETER AddParents
    The parent Ids to add
 
    .PARAMETER RemoveParents
    The parent Ids to remove
 
    .PARAMETER CopyRequiresWriterPermission
    Whether the options to copy, print, or download this file, should be disabled for readers and commenters.
 
    .PARAMETER Starred
    Whether the user has starred the file.
 
    .PARAMETER Trashed
    Whether the file has been trashed, either explicitly or from a trashed parent folder.
 
    Only the owner may trash a file, and other users cannot see files in the owner's trash.
 
    .PARAMETER WritersCanShare
    If $true, sets Writers Can Share to true on the file.
 
    .PARAMETER Projection
    The defined subset of fields to be returned
 
    Available values are:
    * "Minimal"
    * "Standard"
    * "Full"
    * "Access"
 
    .PARAMETER Fields
    The specific fields to returned
 
    .PARAMETER User
    The email or unique Id of the Drive file owner
 
    .EXAMPLE
    Update-GSDriveFile -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -Name "To-Do Progress"
 
    Updates the Drive file with a new name, "To-Do Progress"
 
    .EXAMPLE
    Update-GSDriveFile -FileId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -Path "C:\Pics\NewPic.png"
 
    Updates the Drive file with the content of the file at that path. In this example, the Drive file is a PNG named "Test.png". This will change the content of the file in Drive to match NewPic.png as well as rename it to "NewPic.png"
    #>

    [OutputType('Google.Apis.Drive.v3.Data.File')]
    [cmdletbinding(DefaultParameterSetName = "Depth")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [string]
        $FileId,
        [parameter(Mandatory = $false,Position = 1)]
        [ValidateScript( { Test-Path $_ })]
        [string]
        $Path,
        [parameter(Mandatory = $false)]
        [string]
        $Name,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [ValidateSet('ChocolateIceCream','OldBrickRed','Cardinal','WildStrawberries','MarsOrange','YellowCab','Spearmint','VernFern','Asparagus','SlimeGreen','DesertSand','Macaroni','SeaFoam','Pool','Denim','RainySky','BlueVelvet','PurpleDino','Mouse','MountainGrey','Earthworm','BubbleGum','PurpleRain','ToyEggplant')]
        [string]
        $FolderColorRgb,
        [parameter(Mandatory = $false)]
        [string[]]
        $AddParents,
        [parameter(Mandatory = $false)]
        [String[]]
        $RemoveParents,
        [parameter(Mandatory = $false)]
        [switch]
        $CopyRequiresWriterPermission,
        [parameter(Mandatory = $false)]
        [switch]
        $Starred,
        [parameter(Mandatory = $false)]
        [switch]
        $Trashed,
        [parameter(Mandatory = $false)]
        [switch]
        $WritersCanShare,
        [parameter(Mandatory = $false,ParameterSetName = "Depth")]
        [Alias('Depth')]
        [ValidateSet("Minimal","Standard","Full","Access")]
        [String]
        $Projection = "Full",
        [parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [ValidateSet("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","thumbnailLink","thumbnailVersion","trashed","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")]
        [String[]]
        $Fields,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $colorDictionary = @{
            ChocolateIceCream = '#ac725e'
            OldBrickRed       = '#d06b64'
            Cardinal          = '#f83a22'
            WildStrawberries  = '#fa573c'
            MarsOrange        = '#ff7537'
            YellowCab         = '#ffad46'
            Spearmint         = '#42d692'
            VernFern          = '#16a765'
            Asparagus         = '#7bd148'
            SlimeGreen        = '#b3dc6c'
            DesertSand        = '#fbe983'
            Macaroni          = '#fad165'
            SeaFoam           = '#92e1c0'
            Pool              = '#9fe1e7'
            Denim             = '#9fc6e7'
            RainySky          = '#4986e7'
            BlueVelvet        = '#9a9cff'
            PurpleDino        = '#b99aff'
            Mouse             = '#8f8f8f'
            MountainGrey      = '#cabdbf'
            Earthworm         = '#cca6ac'
            BubbleGum         = '#f691b2'
            PurpleRain        = '#cd74e6'
            ToyEggplant       = '#a47ae2'
        }
        if ($PSCmdlet.ParameterSetName -eq 'Depth') {
            $fs = switch ($Projection) {
                Standard {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","owners","parents","properties","version","webContentLink","webViewLink")
                }
                Access {
                    @("createdTime","description","fileExtension","id","lastModifyingUser","modifiedTime","name","ownedByMe","owners","parents","permissionIds","permissions","shared","sharedWithMeTime","sharingUser","viewedByMe","viewedByMeTime","viewersCanCopyContent","writersCanShare")
                }
                Full {
                    @("appProperties","capabilities","contentHints","createdTime","description","explicitlyTrashed","fileExtension","folderColorRgb","fullFileExtension","hasAugmentedPermissions","hasThumbnail","headRevisionId","iconLink","id","imageMediaMetadata","isAppAuthorized","kind","lastModifyingUser","md5Checksum","mimeType","modifiedByMe","modifiedByMeTime","modifiedTime","name","originalFilename","ownedByMe","owners","parents","permissionIds","permissions","properties","quotaBytesUsed","shared","sharedWithMeTime","sharingUser","size","spaces","starred","teamDriveId","thumbnailLink","thumbnailVersion","trashed","trashedTime","trashingUser","version","videoMediaMetadata","viewedByMe","viewedByMeTime","viewersCanCopyContent","webContentLink","webViewLink","writersCanShare")
                }
            }
        }
        elseif ($PSBoundParameters.ContainsKey('Fields')) {
            $fs = $Fields
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Drive.v3.Data.File'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object { $body.PSObject.Properties.Name -contains $_ }) {
                switch ($prop) {
                    Name {
                        $body.Name = [String]$Name
                    }
                    FolderColorRgb {
                        $body.FolderColorRgb = $ColorDictionary[$FolderColorRgb]
                    }
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            if ($PSBoundParameters.Keys -contains 'Path') {
                $ioFile = Get-Item $Path
                $contentType = Get-MimeType $ioFile
                if ($PSBoundParameters.Keys -notcontains 'Name') {
                    $body.Name = $ioFile.Name
                }
                $stream = New-Object 'System.IO.FileStream' $ioFile.FullName,'Open','Read'
                $request = $service.Files.Update($body,$FileId,$stream,$contentType)
                $request.QuotaUser = $User
                $request.ChunkSize = 512KB
            }
            else {
                $request = $service.Files.Update($body,$FileId)
            }
            $request.SupportsAllDrives = $true
            if ($fs) {
                $request.Fields = $($fs -join ",")
            }
            if ($AddParents) {
                $request.AddParents = $($AddParents -join ",")
            }
            if ($RemoveParents) {
                $request.RemoveParents = $($RemoveParents -join ",")
            }
            Write-Verbose "Updating file '$FileId' for user '$User'"
            if ($PSBoundParameters.Keys -contains 'Path') {
                $request.Upload() | Out-Null
                $stream.Close()
            }
            else {
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSDriveFile'
function Update-GSDriveRevision {
    <#
    .SYNOPSIS
    Updates a revision with patch semantics
 
    .DESCRIPTION
    Updates a revision with patch semantics
 
    .PARAMETER FileId
    The unique Id of the file to update revisions for
 
    .PARAMETER RevisionId
    The unique Id of the revision to update
 
    .PARAMETER KeepForever
    Whether to keep this revision forever, even if it is no longer the head revision. If not set, the revision will be automatically purged 30 days after newer content is uploaded. This can be set on a maximum of 200 revisions for a file.
 
    This field is only applicable to files with binary content in Drive.
 
    .PARAMETER PublishAuto
    Whether subsequent revisions will be automatically republished. This is only applicable to Google Docs.
 
    .PARAMETER Published
    Whether this revision is published. This is only applicable to Google Docs.
 
    .PARAMETER PublishedOutsideDomain
    Whether this revision is published outside the domain. This is only applicable to Google Docs.
 
    .PARAMETER Fields
    The specific fields to returned
 
    .PARAMETER User
    The email or unique Id of the owner of the Drive file
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSDriveRevision -FileId $fileId -Limit 1 | Update-GSDriveRevision -KeepForever
 
    Sets 'KeepForever' for the oldest revision of the file to 'True'
 
    .EXAMPLE
    Get-GSDriveRevision -FileId $fileId | Select-Object -Last 1 | Update-GSDriveRevision -KeepForever
 
    Sets 'KeepForever' for the newest revision of the file to 'True'
    #>

    [OutputType('Google.Apis.Drive.v3.Data.Revision')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [String]
        $FileId,
        [parameter(Mandatory = $true,Position = 1,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $RevisionId,
        [parameter(Mandatory = $false)]
        [Switch]
        $KeepForever,
        [parameter(Mandatory = $false)]
        [Switch]
        $PublishAuto,
        [parameter(Mandatory = $false)]
        [Switch]
        $Published,
        [parameter(Mandatory = $false)]
        [Switch]
        $PublishedOutsideDomain,
        [parameter(Mandatory = $false)]
        [String[]]
        $Fields = '*',
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Drive.v3.DriveService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        $body = New-Object 'Google.Apis.Drive.v3.Data.Revision'
        foreach ($key in $PSBoundParameters.Keys | Where-Object {$_ -in $body.PSObject.Properties.Name}) {
            $body.$key = $PSBoundParameters[$key]
        }
        foreach ($id in $RevisionId) {
            try {
                $request = $service.Revisions.Update($body,$FileId,$id)
                if ($Fields) {
                    $request.Fields = $($Fields -join ",")
                }
                Write-Verbose "Updating Drive File Revision Id '$revision' of File Id '$FileId' for User '$User'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru | Add-Member -MemberType NoteProperty -Name 'FileId' -Value $FileId -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSDriveRevision'
function Watch-GSDriveUpload {
    <#
    .SYNOPSIS
    Shows progress in the console of current Drive file uploads
 
    .DESCRIPTION
    Shows progress in the console of current Drive file uploads
 
    .PARAMETER Id
    The upload Id(s) that you would like to watch
 
    .PARAMETER Action
    Whether the action is uploading or retrying. This is mainly for use in Start-GSDriveFileUpload and defaults to 'Uploading'
 
    .PARAMETER CountUploaded
    Current file count being uploaded
 
    .PARAMETER TotalUploading
    Total file count being uploaded
 
    .EXAMPLE
    Watch-GSDriveUpload
 
    Watches the files currently being uploaded from the active session
    #>

    [CmdletBinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Int[]]
        $Id,
        [parameter(Mandatory = $false)]
        [ValidateSet('Uploading','Retrying')]
        [String]
        $Action = "Uploading",
        [parameter(Mandatory = $false)]
        [Int]
        $CountUploaded,
        [parameter(Mandatory = $false)]
        [Int]
        $TotalUploading
    )
    Begin {
        Write-Verbose "Watching Drive File Upload"
    }
    Process {
        do {
            $i = 1
            if ($PSBoundParameters.Keys -contains 'Id') {
                $statusList = Get-GSDriveFileUploadStatus -Id @($Id)
            }
            else {
                $statusList = Get-GSDriveFileUploadStatus
            }
            if ($statusList) {
                $totalPercent = 0
                $totalSecondsRemaining = 0
                $count = 0
                $statusList | ForEach-Object {
                    $count++
                    $totalPercent += $_.PercentComplete
                    $totalSecondsRemaining += $_.Remaining.TotalSeconds
                }
                $curCount = if ($PSBoundParameters.Keys -contains 'CountUploaded') {
                    $CountUploaded
                }
                else {
                    $count
                }
                $totalCount = if ($PSBoundParameters.Keys -contains 'TotalUploading') {
                    $TotalUploading
                }
                else {
                    $count
                }
                $totalPercent = $totalPercent / $totalCount
                $totalSecondsRemaining = $totalSecondsRemaining / $totalCount
                $parentParams = @{
                    Activity         = "[$([Math]::Round($totalPercent,4))%] $Action [$curCount / $totalCount] files to Google Drive"
                    SecondsRemaining = $($statusList.Remaining.TotalSeconds | Sort-Object | Select-Object -Last 1)
                }
                if (!($statusList | Where-Object {$_.Status -ne "Completed"})) {
                    $parentParams['Completed'] = $true
                }
                else {
                    $parentParams['PercentComplete'] = [Math]::Round($totalPercent,4)
                }
                if ($psEditor -or $IsMacOS -or $IsLinux) {
                    Write-InlineProgress @parentParams
                }
                else {
                    $parentParams['Id'] = 1
                    Write-Progress @parentParams
                }
                if (!$psEditor -and !$IsMacOS -and !$IsLinux -and ($statusList.Count -le 5)) {
                    foreach ($status in $statusList) {
                        $i++
                        $statusFmt = if ($status.Status -eq "Completed") {
                            "Completed uploading"
                        }
                        else {
                            $status.Status
                        }
                        $progParams = @{
                            Activity         = "[$($status.PercentComplete)%] [ID: $($status.Id)] $($statusFmt) file '$($status.File)' to Google Drive$(if($status.Parents){" (Parents: '$($status.Parents -join "', '")')"})"
                            SecondsRemaining = $status.Remaining.TotalSeconds
                            Id               = $i
                            ParentId         = 1
                        }
                        if ($_.Status -eq "Completed") {
                            $progParams['Completed'] = $true
                        }
                        else {
                            $progParams['PercentComplete'] = [Math]::Round($status.PercentComplete,4)
                        }
                        Write-Progress @progParams
                    }
                }
                Start-Sleep -Seconds 1
            }
        }
        until (!$statusList -or !($statusList | Where-Object {$_.Status -notin @("Failed","Completed")}))
    }
}

Export-ModuleMember -Function 'Watch-GSDriveUpload'
function Add-GSGmailFilter {
    <#
    .SYNOPSIS
    Adds a new Gmail filter
 
    .DESCRIPTION
    Adds a new Gmail filter
 
    .PARAMETER User
    The email of the user you are adding the filter for
 
    .PARAMETER From
    The sender's display name or email address.
 
    .PARAMETER To
    The recipient's display name or email address. Includes recipients in the "to", "cc", and "bcc" header fields. You can use simply the local part of the email address. For example, "example" and "example@" both match "example@gmail.com". This field is case-insensitive
 
    .PARAMETER Subject
    Case-insensitive phrase found in the message's subject. Trailing and leading whitespace are be trimmed and adjacent spaces are collapsed
 
    .PARAMETER Query
    Only return messages matching the specified query. Supports the same query format as the Gmail search box. For example, "from:someuser@example.com rfc822msgid: is:unread"
 
    .PARAMETER NegatedQuery
    Only return messages not matching the specified query. Supports the same query format as the Gmail search box. For example, "from:someuser@example.com rfc822msgid: is:unread"
 
    .PARAMETER HasAttachment
    Whether the message has any attachment
 
    .PARAMETER ExcludeChats
    Whether the response should exclude chats
 
    .PARAMETER AddLabelIDs
    List of labels to add to the message
 
    .PARAMETER RemoveLabelIDs
    List of labels to remove from the message
 
    .PARAMETER Forward
    Email address that the message should be forwarded to
 
    .PARAMETER Size
    The size of the entire RFC822 message in bytes, including all headers and attachments
 
    .PARAMETER SizeComparison
    How the message size in bytes should be in relation to the size field.
 
    Acceptable values are:
    * "larger"
    * "smaller"
    * "unspecified"
 
    .PARAMETER Raw
    If $true, returns the raw response. If not passed or -Raw:$false, response is formatted as a flat object for readability
 
    .EXAMPLE
    Add-GSGmailFilter -To admin@domain.com -ExcludeChats -Forward "admin_directMail@domain.com"
 
    Adds a filter for the AdminEmail user to forward all mail sent directly to the to "admin_directMail@domain.com"
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Filter')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $From,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $To,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $Subject,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $Query,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $NegatedQuery,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Switch]
        $HasAttachment,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Switch]
        $ExcludeChats,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $AddLabelIDs,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $RemoveLabelIDs,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $Forward,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [int]
        $Size,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('Larger','Smaller','Unspecified')]
        [string]
        $SizeComparison,
        [parameter(Mandatory = $false)]
        [Switch]
        $Raw
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Gmail.v1.Data.Filter'
            $action = New-Object 'Google.Apis.Gmail.v1.Data.FilterAction'
            $criteria = New-Object 'Google.Apis.Gmail.v1.Data.FilterCriteria'
            foreach ($key in $PSBoundParameters.Keys) {
                switch ($key) {
                    AddLabelIDs {
                        $action.$key = [String[]]($PSBoundParameters[$key])
                    }
                    RemoveLabelIds {
                        $action.$key = [String[]]($PSBoundParameters[$key])
                    }
                    Forward {
                        $action.$key = $PSBoundParameters[$key]
                    }
                    Default {
                        if ($criteria.PSObject.Properties.Name -contains $key) {
                            $criteria.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            $body.Action = $action
            $body.Criteria = $criteria
            $request = $service.Users.Settings.Filters.Create($body,$User)
            Write-Verbose "Creating Filter for user '$User'"
            $response = $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            if (!$Raw) {
                $response = $response | Select-Object User,Id,@{N = "From";E = {$_.criteria.from}},@{N = "To";E = {$_.criteria.to}},@{N = "Subject";E = {$_.criteria.subject}},@{N = "Query";E = {$_.criteria.query}},@{N = "NegatedQuery";E = {$_.criteria.negatedQuery}},@{N = "HasAttachment";E = {$_.criteria.hasAttachment}},@{N = "ExcludeChats";E = {$_.criteria.excludeChats}},@{N = "Size";E = {$_.criteria.size}},@{N = "SizeComparison";E = {$_.criteria.sizeComparison}},@{N = "AddLabelIds";E = {$_.action.addLabelIds}},@{N = "RemoveLabelIds";E = {$_.action.removeLabelIds}},@{N = "Forward";E = {$_.action.forward}}
            }
            $response
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSGmailFilter'
function Add-GSGmailForwardingAddress {
    <#
    .SYNOPSIS
    Creates a forwarding address.
 
    .DESCRIPTION
    Creates a forwarding address. If ownership verification is required, a message will be sent to the recipient and the resource's verification status will be set to pending; otherwise, the resource will be created with verification status set to accepted.
 
    .PARAMETER ForwardingAddress
    An email address to which messages can be forwarded.
 
    .PARAMETER User
    The user to create the forwarding addresses for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Add-GSGmailForwardingAddress "joe@domain.com"
 
    Adds joe@domain.com as a forwarding address for the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.ForwardingAddress')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("Id")]
        [string[]]
        $ForwardingAddress,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.sharing'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            foreach ($fwd in $ForwardingAddress) {
                $body = New-Object 'Google.Apis.Gmail.v1.Data.ForwardingAddress' -Property @{
                    ForwardingEmail = $fwd
                }
                $request = $service.Users.Settings.ForwardingAddresses.Create($body,$User)
                Write-Verbose "Creating Forwarding Address '$fwd' for user '$User'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSGmailForwardingAddress'
function Get-GSGmailAutoForwardingSettings {
    <#
    .SYNOPSIS
    Gets AutoForwarding settings
 
    .DESCRIPTION
    Gets AutoForwarding settings
 
    .PARAMETER User
    The user to get the AutoForwarding settings for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSGmailAutoForwardingSettings
 
    Gets the AutoForwarding settings for the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.AutoForwarding')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $request = $service.Users.Settings.GetAutoForwarding($User)
            Write-Verbose "Getting AutoForwarding settings for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailAutoForwardingSettings'
function Get-GSGmailFilter {
    <#
    .SYNOPSIS
    Gets Gmail filter details
 
    .DESCRIPTION
    Gets Gmail filter details
 
    .PARAMETER FilterId
    The unique Id of the filter you would like to retrieve information for. If excluded, all filters for the user are returned
 
    .PARAMETER User
    The email of the user you are getting the filter information for
 
    .PARAMETER Raw
    If $true, returns the raw response. If not passed or -Raw:$false, response is formatted as a flat object for readability
 
    .EXAMPLE
    Get-GSGmailFilter -User joe
 
    Gets the list of filters for Joe
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Filter')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("Id")]
        [string[]]
        $FilterId,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($FilterId) {
                foreach ($fil in $FilterId) {
                    $request = $service.Users.Settings.Filters.Get($User,$fil)
                    Write-Verbose "Getting Filter Id '$fil' for user '$User'"
                    $response = $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                    if (!$Raw) {
                        $response = $response | Select-Object User,Id,@{N = "From";E = {$_.criteria.from}},@{N = "To";E = {$_.criteria.to}},@{N = "Subject";E = {$_.criteria.subject}},@{N = "Query";E = {$_.criteria.query}},@{N = "NegatedQuery";E = {$_.criteria.negatedQuery}},@{N = "HasAttachment";E = {$_.criteria.hasAttachment}},@{N = "ExcludeChats";E = {$_.criteria.excludeChats}},@{N = "Size";E = {$_.criteria.size}},@{N = "SizeComparison";E = {$_.criteria.sizeComparison}},@{N = "AddLabelIds";E = {$_.action.addLabelIds}},@{N = "RemoveLabelIds";E = {$_.action.removeLabelIds}},@{N = "Forward";E = {$_.action.forward}}
                    }
                    $response
                }
            }
            else {
                $request = $service.Users.Settings.Filters.List($User)
                Write-Verbose "Getting Filter List for user '$User'"
                $response = $request.Execute() | Select-Object -ExpandProperty Filter | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                if (!$Raw) {
                    $response = $response | Select-Object User,Id,@{N = "From";E = {$_.criteria.from}},@{N = "To";E = {$_.criteria.to}},@{N = "Subject";E = {$_.criteria.subject}},@{N = "Query";E = {$_.criteria.query}},@{N = "NegatedQuery";E = {$_.criteria.negatedQuery}},@{N = "HasAttachment";E = {$_.criteria.hasAttachment}},@{N = "ExcludeChats";E = {$_.criteria.excludeChats}},@{N = "Size";E = {$_.criteria.size}},@{N = "SizeComparison";E = {$_.criteria.sizeComparison}},@{N = "AddLabelIds";E = {$_.action.addLabelIds}},@{N = "RemoveLabelIds";E = {$_.action.removeLabelIds}},@{N = "Forward";E = {$_.action.forward}}
                }
                $response
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailFilter'
function Get-GSGmailForwardingAddress {
    <#
    .SYNOPSIS
    Gets Gmail forwarding address information for the user
 
    .DESCRIPTION
    Gets Gmail forwarding address information for the user
 
    .PARAMETER ForwardingAddress
    The forwarding address you would like to get info for. If excluded, gets the list of forwarding addresses and their info for the user
 
    .PARAMETER User
    The user to get the forwarding addresses for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSGmailForwardingAddress
 
    Gets the list of forwarding addresses for the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.ForwardingAddress')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("Id")]
        [string[]]
        $ForwardingAddress,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($ForwardingAddress) {
                foreach ($fwd in $ForwardingAddress) {
                    $request = $service.Users.Settings.ForwardingAddresses.Get($User,$fwd)
                    Write-Verbose "Getting Forwarding Address '$fwd' for user '$User'"
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                }
            }
            else {
                $request = $service.Users.Settings.ForwardingAddresses.List($User)
                Write-Verbose "Getting Forwarding Address List for user '$User'"
                $request.Execute() | Select-Object -ExpandProperty ForwardingAddresses | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailForwardingAddress'
function Get-GSGmailImapSettings {
    <#
    .SYNOPSIS
    Gets IMAP settings
 
    .DESCRIPTION
    Gets IMAP settings
 
    .PARAMETER User
    The user to get the IMAP settings for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSGmailImapSettings
 
    Gets the IMAP settings for the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.ImapSettings')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $request = $service.Users.Settings.GetImap($User)
            Write-Verbose "Getting IMAP settings for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailImapSettings'
function Get-GSGmailLabel {
    <#
    .SYNOPSIS
    Gets Gmail label information for the user
 
    .DESCRIPTION
    Gets Gmail label information for the user
 
    .PARAMETER LabelId
    The unique Id of the label to get information for. If excluded, returns the list of labels for the user
 
    .PARAMETER User
    The user to get label information for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSGmailLabel
 
    Gets the Gmail labels of the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Label')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("Id")]
        [string[]]
        $LabelId,
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($LabelId) {
                foreach ($label in $LabelId) {
                    $request = $service.Users.Labels.Get($User,$label)
                    Write-Verbose "Getting Label Id '$label' for user '$User'"
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                }
            }
            else {
                $request = $service.Users.Labels.List($User)
                Write-Verbose "Getting Label List for user '$User'"
                $request.Execute() | Select-Object -ExpandProperty Labels | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailLabel'
function Get-GSGmailLanguageSettings {
    <#
    .SYNOPSIS
    Gets Gmail display language settings
 
    .DESCRIPTION
    Gets Gmail display language settings
 
    .PARAMETER User
    The user to get the Gmail display language settings for.
 
    Defaults to the AdminEmail user.
 
    .EXAMPLE
    Get-GSGmailLanguageSettings -User me
 
    Gets the Gmail display language for the AdminEmail user.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.LanguageSettings')]
    [cmdletbinding()]
    Param (
        [parameter(Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
                ServiceType = 'Google.Apis.Gmail.v1.GmailService'
                User        = $U
            }
            $service = New-GoogleService @serviceParams
            try {
                $request = $service.Users.Settings.GetLanguage($U)
                Write-Verbose "Getting Gmail Display Language for user '$U'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailLanguageSettings'
function Get-GSGmailMessage {
    <#
    .SYNOPSIS
    Gets Gmail message details
 
    .DESCRIPTION
    Gets Gmail message details
 
    .PARAMETER User
    The primary email of the user who owns the message
 
    Defaults to the AdminEmail user
 
    .PARAMETER Id
    The Id of the message to retrieve info for
 
    .PARAMETER ParseMessage
    If $true, returns the parsed raw message
 
    .PARAMETER SaveAttachmentsTo
    If the message has attachments, the path to save the attachments to. If excluded, attachments are not saved locally
 
    .PARAMETER Format
    The format of the message metadata to retrieve
 
    Available values are:
    * "Full"
    * "Metadata"
    * "Minimal"
    * "Raw"
 
    Defaults to "Full", but forces -Format as "Raw" if -ParseMessage or -SaveAttachmentsTo are used
 
    .EXAMPLE
    Get-GSGmailMessage -Id 1615f9a6ee36cb5b -ParseMessage
 
    Gets the full message details for the provided Id and parses out the raw MIME message content
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Message')]
    [cmdletbinding(DefaultParameterSetName = "Format")]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('MessageId')]
        [String[]]
        $Id,
        [parameter(Mandatory = $false,ParameterSetName = "ParseMessage")]
        [switch]
        $ParseMessage,
        [parameter(Mandatory = $false,ParameterSetName = "ParseMessage")]
        [Alias('AttachmentOutputPath','OutFilePath')]
        [ValidateScript({(Get-Item $_).PSIsContainer})]
        [string]
        $SaveAttachmentsTo,
        [parameter(Mandatory = $false,ParameterSetName = "Format")]
        [ValidateSet("Full","Metadata","Minimal","Raw")]
        [string]
        $Format = "Full"
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        if ($ParseMessage -or $SaveAttachmentsTo) {
            $Format = "Raw"
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            foreach ($mId in $Id) {
                $request = $service.Users.Messages.Get($User,$mId)
                $request.Format = $Format
                foreach ($key in $PSBoundParameters.Keys | Where-Object {$_ -ne "Id"}) {
                    switch ($key) {
                        Default {
                            if ($request.PSObject.Properties.Name -contains $key) {
                                $request.$key = $PSBoundParameters[$key]
                            }
                        }
                    }
                }
                Write-Verbose "Getting Message Id '$mId' for user '$User'"
                $result = $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                if ($ParseMessage -or $SaveAttachmentsTo) {
                    $parsed = Read-MimeMessage -String $(Convert-Base64 -From WebSafeBase64String -To NormalString -String $result.Raw) | Select-Object @{N = 'User';E = {$User}},@{N = "Id";E = {$result.Id}},@{N = "ThreadId";E = {$result.ThreadId}},@{N = "LabelIds";E = {$result.LabelIds}},@{N = "Snippet";E = {$result.Snippet}},@{N = "HistoryId";E = {$result.HistoryId}},@{N = "InternalDate";E = {$result.InternalDate}},@{N = "InternalDateConverted";E = {Convert-EpochToDate -EpochString $result.internalDate}},@{N = "SizeEstimate";E = {$result.SizeEstimate}},*
                    if ($SaveAttachmentsTo) {
                        $resPath = Resolve-Path $SaveAttachmentsTo
                        $attachments = New-Object System.Collections.Generic.List[object]
                        if ($parsed.Attachments | Where-Object {$_.FileName}) {
                            $parsed.Attachments | Where-Object {$_.FileName} | ForEach-Object {
                                $attachments.Add($_)
                            }
                        }
                        if ($parsed.BodyParts | Where-Object {$_.FileName}) {
                            $parsed.BodyParts | Where-Object {$_.FileName} | ForEach-Object {
                                $attachments.Add($_)
                            }
                        }
                        $attachments = $attachments | Sort-Object {"$($_.FileName)_$($_.ContentId)"} -Unique
                        foreach ($att in $attachments) {
                            $cleanedName = Get-SafeFileName $att.FileName
                            $fileName = Join-Path $resPath $cleanedName
                            Write-Verbose "Saving attachment to path '$fileName'"
                            $stream = [System.IO.File]::Create($fileName)
                            $att.ContentObject.DecodeTo($stream)
                            $stream.Close()
                        }
                    }
                    $parsed
                }
                else {
                    $result
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailMessage'
function Get-GSGmailMessageList {
    <#
    .SYNOPSIS
    Gets a list of messages
 
    .DESCRIPTION
    Gets a list of messages
 
    .PARAMETER User
    The primary email of the user to list messages for
 
    Defaults to the AdminEmail user
 
    .PARAMETER Filter
    Only return messages matching the specified query. Supports the same query format as the Gmail search box. For example, "from:someuser@example.com rfc822msgid:<lkj123l4jj1lj@gmail.com> is:unread"
 
    More info on Gmail search operators here: https://support.google.com/mail/answer/7190?hl=en
 
    .PARAMETER Rfc822MsgId
    The RFC822 Message ID to add to your filter.
 
    .PARAMETER LabelIds
    Only return messages with labels that match all of the specified label IDs
 
    .PARAMETER ExcludeChats
    Exclude chats from the message list
 
    .PARAMETER IncludeSpamTrash
    Include messages from SPAM and TRASH in the results
 
    .PARAMETER PageSize
    The page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSGmailMessageList -Filter "to:me","after:2017/12/25" -ExcludeChats
 
    Gets the list of messages sent directly to the user after 2017/12/25 excluding chats
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Message')]
    [cmdletbinding(DefaultParameterSetName = "Filter")]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ParameterSetName = "Filter")]
        [Alias('Query')]
        [String[]]
        $Filter,
        [parameter(Mandatory = $false,ParameterSetName = "Rfc822MsgId")]
        [Alias('MessageId','MsgId')]
        [String]
        $Rfc822MsgId,
        [parameter(Mandatory = $false)]
        [Alias('LabelId')]
        [String[]]
        $LabelIds,
        [parameter(Mandatory = $false)]
        [switch]
        $ExcludeChats,
        [parameter(Mandatory = $false)]
        [switch]
        $IncludeSpamTrash,
        [parameter(Mandatory = $false)]
        [ValidateRange(1,500)]
        [Int]
        $PageSize = 500,
        [parameter(Mandatory = $false)]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                $serviceParams = @{
                    Scope       = 'https://mail.google.com'
                    ServiceType = 'Google.Apis.Gmail.v1.GmailService'
                    User        = $U
                }
                if ($ExcludeChats) {
                    if ($Filter) {
                        $Filter += "-in:chats"
                    }
                    else {
                        $Filter = "-in:chats"
                    }
                }
                $service = New-GoogleService @serviceParams
                $request = $service.Users.Messages.List($U)
                foreach ($key in $PSBoundParameters.Keys) {
                    switch ($key) {
                        Rfc822MsgId {
                            $request.Q = "rfc822msgid:$($Rfc822MsgId.Trim())"
                        }
                        Filter {
                            $request.Q = $($Filter -join " ")
                        }
                        LabelIds {
                            $request.LabelIds = [String[]]$LabelIds
                        }
                        Default {
                            if ($request.PSObject.Properties.Name -contains $key) {
                                $request.$key = $PSBoundParameters[$key]
                            }
                        }
                    }
                }
                if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                    $PageSize = $Limit
                }
                $request.MaxResults = $PageSize
                if ($request.Q) {
                    Write-Verbose "Getting all Messages matching filter '$($request.Q)' for user '$U'"
                }
                else {
                    Write-Verbose "Getting all Messages for user '$U'"
                }
                [int]$i = 1
                $overLimit = $false
                do {
                    $result = $request.Execute()
                    if ($result.Messages) {
                        $result.Messages | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru | Add-Member -MemberType NoteProperty -Name 'Filter' -Value $Filter -PassThru
                    }
                    if ($result.NextPageToken) {
                        $request.PageToken = $result.NextPageToken
                    }
                    [int]$retrieved = ($i + $result.Messages.Count) - 1
                    Write-Verbose "Retrieved $retrieved Messages..."
                    if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                        Write-Verbose "Limit reached: $Limit"
                        $overLimit = $true
                    }
                    elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                        $newPS = $Limit - $retrieved
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                        $request.MaxResults = $newPS
                    }
                    [int]$i = $i + $result.Messages.Count
                }
                until ($overLimit -or !$result.NextPageToken)
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailMessageList'
function Get-GSGmailPopSettings {
    <#
    .SYNOPSIS
    Gets POP settings
 
    .DESCRIPTION
    Gets POP settings
 
    .PARAMETER User
    The user to get the POP settings for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSGmailPopSettings
 
    Gets the POP settings for the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.PopSettings')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $request = $service.Users.Settings.GetPop($User)
            Write-Verbose "Getting POP settings for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailPopSettings'
function Get-GSGmailProfile {
    <#
    .SYNOPSIS
    Gets Gmail profile for the user
 
    .DESCRIPTION
    Gets Gmail profile for the user
 
    .PARAMETER User
    The user to get profile of
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSGmailProfile
 
    Gets the Gmail profile of the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Profile')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        foreach ($U in $User) {
            try {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                $serviceParams = @{
                    Scope       = 'https://mail.google.com'
                    ServiceType = 'Google.Apis.Gmail.v1.GmailService'
                    User        = $U
                }
                $service = New-GoogleService @serviceParams
                $request = $service.Users.GetProfile($U)
                Write-Verbose "Getting Gmail profile for user '$U'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailProfile'
function Get-GSGmailSendAsAlias {
    <#
    .SYNOPSIS
    Gets SendAs alias settings for a user.
 
    .DESCRIPTION
    Gets SendAs alias settings for a user.
 
    .PARAMETER SendAsEmail
    The SendAs alias to be retrieved.
 
    If excluded, gets the list of SendAs aliases.
 
    .PARAMETER User
    The email of the user you are getting the information for
 
    .EXAMPLE
    Get-GSGmailSendAsSettings -User joe@domain.com
 
    Gets the list of SendAs Settings for Joe
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.SendAs')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("SendAs")]
        [string[]]
        $SendAsEmail,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        if ($PSBoundParameters.ContainsKey('SendAsEmail')) {
            foreach ($sendAs in $SendAsEmail) {
                try {
                    if ($sendAs -notlike "*@*.*") {
                        $sendAs = "$($sendAs)@$($Script:PSGSuite.Domain)"
                    }
                    $request = $service.Users.Settings.SendAs.Get($User,$sendAs)
                    Write-Verbose "Getting SendAs settings of alias '$sendAs' for user '$User'"
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
        else {
            try {
                $request = $service.Users.Settings.SendAs.List($User)
                Write-Verbose "Getting SendAs List for user '$User'"
                $request.Execute() | Select-Object -ExpandProperty SendAs | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailSendAsAlias'
function Get-GSGmailSMIMEInfo {
    <#
    .SYNOPSIS
    Gets Gmail S/MIME info
 
    .DESCRIPTION
    Gets Gmail S/MIME info
 
    .PARAMETER SendAsEmail
    The email address that appears in the "From:" header for mail sent using this alias.
 
    .PARAMETER Id
    The immutable ID for the SmimeInfo.
 
    If left blank, returns the list of S/MIME infos for the SendAsEmail and User
 
    .PARAMETER User
    The user's email address
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSGmailSMIMEInfo -SendAsEmail 'joe@otherdomain.com' -User joe@domain.com
 
    Gets the list of S/MIME infos for Joe's SendAsEmail 'joe@otherdomain.com'
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.SmimeInfo')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true)]
        [string]
        $SendAsEmail,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Id,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($PSBoundParameters.Keys -contains 'Id') {
                foreach ($I in $Id) {
                    $request = $service.Users.Settings.SendAs.SmimeInfo.Get($User,$SendAsEmail,$I)
                    Write-Verbose "Getting S/MIME Id '$I' of SendAsEmail '$SendAsEmail' for user '$User'"
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                }
            }
            else {
                $request = $service.Users.Settings.SendAs.SmimeInfo.List($User,$SendAsEmail)
                Write-Verbose "Getting list of S/MIME Id's of SendAsEmail '$SendAsEmail' for user '$User'"
                $request.Execute() | Select-Object -ExpandProperty smimeInfo | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailSMIMEInfo'
function Get-GSGmailVacationSettings {
    <#
    .SYNOPSIS
    Gets Vacation settings
 
    .DESCRIPTION
    Gets Vacation settings
 
    .PARAMETER User
    The user to get the Vacation settings for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Get-GSGmailVacationSettings
 
    Gets the Vacation settings for the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.VacationSettings')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $request = $service.Users.Settings.GetVacation($User)
            Write-Verbose "Getting Vacation settings for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGmailVacationSettings'
function New-GSGmailLabel {
    <#
    .SYNOPSIS
    Adds a Gmail label
 
    .DESCRIPTION
    Adds a Gmail label
 
    .PARAMETER Name
    The name of the label to add
 
    .PARAMETER LabelListVisibility
    The visibility of the label in the label list in the Gmail web interface.
 
    Acceptable values are:
    * "labelHide": Do not show the label in the label list.
    * "labelShow": Show the label in the label list. (Default)
    * "labelShowIfUnread": Show the label if there are any unread messages with that label.
 
    .PARAMETER MessageListVisibility
    The visibility of messages with this label in the message list in the Gmail web interface.
 
    Acceptable values are:
    * "hide": Do not show the label in the message list.
    * "show": Show the label in the message list. (Default)
 
    .PARAMETER BackgroundColor
    The background color of the label
 
    Options and their corresponding hex code:
 
        Amethyst = '#8e63ce'
        BananaMania = '#fce8b3'
        Bermuda = '#68dfa9'
        BilobaFlower = '#b694e8'
        Black = '#000000'
        BlueRomance = '#c6f3de'
        BrandyPunch = '#cf8933'
        BurntSienna = '#e66550'
        Cadillac = '#b65775'
        Camelot = '#83334c'
        CeruleanBlue = '#285bac'
        ChathamsBlue = '#1c4587'
        Concrete = '#f3f3f3'
        CornflowerBlue = '#4a86e8'
        LightCornflowerBlue = '#6d9eeb'
        CreamCan = '#f2c960'
        Cupid = '#fbc8d9'
        DeepBlush = '#e07798'
        Desert = '#a46a21'
        DoveGray = '#666666'
        DustyGray = '#999999'
        Eucalyptus = '#2a9c68'
        Flesh = '#ffd6a2'
        FringyFlower = '#b9e4d0'
        Gallery = '#efefef'
        Goldenrod = '#fad165'
        Illusion = '#f7a7c0'
        Jewel = '#1a764d'
        Koromiko = '#ffbc6b'
        LightMountainMeadow = '#16a766'
        LightShamrock = '#43d692'
        LuxorGold = '#aa8831'
        MandysPink = '#f6c5be'
        MediumPurple = '#a479e2'
        Meteorite = '#41236d'
        MoonRaker = '#d0bcf1'
        LightMoonRaker = '#e4d7f5'
        MountainMeadow = '#149e60'
        Oasis = '#fef1d1'
        OceanGreen = '#44b984'
        OldGold = '#d5ae49'
        Perano = '#a4c2f4'
        PersianPink = '#f691b3'
        PigPink = '#fcdee8'
        Pueblo = '#822111'
        RedOrange = '#fb4c2f'
        RoyalBlue = '#3c78d8'
        RoyalPurple = '#653e9b'
        Salem = '#0b804b'
        Salomie = '#fcda83'
        SeaPink = '#efa093'
        Shamrock = '#3dc789'
        Silver = '#cccccc'
        Tabasco = '#ac2b16'
        Tequila = '#ffe6c7'
        Thunderbird = '#cc3a21'
        TropicalBlue = '#c9daf8'
        TulipTree = '#eaa041'
        Tundora = '#434343'
        VistaBlue = '#89d3b2'
        Watercourse = '#076239'
        WaterLeaf = '#a0eac9'
        White = '#ffffff'
        YellowOrange = '#ffad47'
 
    .PARAMETER TextColor
    The text color of the label
 
    Options and their corresponding hex code:
 
        Amethyst = '#8e63ce'
        BananaMania = '#fce8b3'
        Bermuda = '#68dfa9'
        BilobaFlower = '#b694e8'
        Black = '#000000'
        BlueRomance = '#c6f3de'
        BrandyPunch = '#cf8933'
        BurntSienna = '#e66550'
        Cadillac = '#b65775'
        Camelot = '#83334c'
        CeruleanBlue = '#285bac'
        ChathamsBlue = '#1c4587'
        Concrete = '#f3f3f3'
        CornflowerBlue = '#4a86e8'
        LightCornflowerBlue = '#6d9eeb'
        CreamCan = '#f2c960'
        Cupid = '#fbc8d9'
        DeepBlush = '#e07798'
        Desert = '#a46a21'
        DoveGray = '#666666'
        DustyGray = '#999999'
        Eucalyptus = '#2a9c68'
        Flesh = '#ffd6a2'
        FringyFlower = '#b9e4d0'
        Gallery = '#efefef'
        Goldenrod = '#fad165'
        Illusion = '#f7a7c0'
        Jewel = '#1a764d'
        Koromiko = '#ffbc6b'
        LightMountainMeadow = '#16a766'
        LightShamrock = '#43d692'
        LuxorGold = '#aa8831'
        MandysPink = '#f6c5be'
        MediumPurple = '#a479e2'
        Meteorite = '#41236d'
        MoonRaker = '#d0bcf1'
        LightMoonRaker = '#e4d7f5'
        MountainMeadow = '#149e60'
        Oasis = '#fef1d1'
        OceanGreen = '#44b984'
        OldGold = '#d5ae49'
        Perano = '#a4c2f4'
        PersianPink = '#f691b3'
        PigPink = '#fcdee8'
        Pueblo = '#822111'
        RedOrange = '#fb4c2f'
        RoyalBlue = '#3c78d8'
        RoyalPurple = '#653e9b'
        Salem = '#0b804b'
        Salomie = '#fcda83'
        SeaPink = '#efa093'
        Shamrock = '#3dc789'
        Silver = '#cccccc'
        Tabasco = '#ac2b16'
        Tequila = '#ffe6c7'
        Thunderbird = '#cc3a21'
        TropicalBlue = '#c9daf8'
        TulipTree = '#eaa041'
        Tundora = '#434343'
        VistaBlue = '#89d3b2'
        Watercourse = '#076239'
        WaterLeaf = '#a0eac9'
        White = '#ffffff'
        YellowOrange = '#ffad47'
 
    .PARAMETER User
    The primary email of the user to add the label to
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    New-GSGmailLabel -Name Label1
 
    Adds the label "Label1" to the AdminEmail
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Label')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true, Position = 0)]
        [string]
        $Name,
        [parameter(Mandatory = $false)]
        [ValidateSet('labelHide','labelShow','labelShowIfUnread')]
        [string]
        $LabelListVisibility = "labelShow",
        [parameter(Mandatory = $false)]
        [ValidateSet('show','hide')]
        [string]
        $MessageListVisibility = "show",
        [parameter(Mandatory = $false)]
        [ValidateSet('Amethyst','BananaMania','Bermuda','BilobaFlower','Black','BlueRomance','BrandyPunch','BurntSienna','Cadillac','Camelot','CeruleanBlue','ChathamsBlue','Concrete','CornflowerBlue','CreamCan','Cupid','DeepBlush','Desert','DoveGray','DustyGray','Eucalyptus','Flesh','FringyFlower','Gallery','Goldenrod','Illusion','Jewel','Koromiko','LightCornflowerBlue','LightMoonRaker','LightMountainMeadow','LightShamrock','LuxorGold','MandysPink','MediumPurple','Meteorite','MoonRaker','MountainMeadow','Oasis','OceanGreen','OldGold','Perano','PersianPink','PigPink','Pueblo','RedOrange','RoyalBlue','RoyalPurple','Salem','Salomie','SeaPink','Shamrock','Silver','Tabasco','Tequila','Thunderbird','TropicalBlue','TulipTree','Tundora','VistaBlue','Watercourse','WaterLeaf','White','YellowOrange')]
        [string]
        $BackgroundColor,
        [parameter(Mandatory = $false)]
        [ValidateSet('Amethyst','BananaMania','Bermuda','BilobaFlower','Black','BlueRomance','BrandyPunch','BurntSienna','Cadillac','Camelot','CeruleanBlue','ChathamsBlue','Concrete','CornflowerBlue','CreamCan','Cupid','DeepBlush','Desert','DoveGray','DustyGray','Eucalyptus','Flesh','FringyFlower','Gallery','Goldenrod','Illusion','Jewel','Koromiko','LightCornflowerBlue','LightMoonRaker','LightMountainMeadow','LightShamrock','LuxorGold','MandysPink','MediumPurple','Meteorite','MoonRaker','MountainMeadow','Oasis','OceanGreen','OldGold','Perano','PersianPink','PigPink','Pueblo','RedOrange','RoyalBlue','RoyalPurple','Salem','Salomie','SeaPink','Shamrock','Silver','Tabasco','Tequila','Thunderbird','TropicalBlue','TulipTree','Tundora','VistaBlue','Watercourse','WaterLeaf','White','YellowOrange')]
        [string]
        $TextColor,
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $colorDict = @{
            Amethyst            = '#8e63ce'
            BananaMania         = '#fce8b3'
            Bermuda             = '#68dfa9'
            BilobaFlower        = '#b694e8'
            Black               = '#000000'
            BlueRomance         = '#c6f3de'
            BrandyPunch         = '#cf8933'
            BurntSienna         = '#e66550'
            Cadillac            = '#b65775'
            Camelot             = '#83334c'
            CeruleanBlue        = '#285bac'
            ChathamsBlue        = '#1c4587'
            Concrete            = '#f3f3f3'
            CornflowerBlue      = '#4a86e8'
            LightCornflowerBlue = '#6d9eeb'
            CreamCan            = '#f2c960'
            Cupid               = '#fbc8d9'
            DeepBlush           = '#e07798'
            Desert              = '#a46a21'
            DoveGray            = '#666666'
            DustyGray           = '#999999'
            Eucalyptus          = '#2a9c68'
            Flesh               = '#ffd6a2'
            FringyFlower        = '#b9e4d0'
            Gallery             = '#efefef'
            Goldenrod           = '#fad165'
            Illusion            = '#f7a7c0'
            Jewel               = '#1a764d'
            Koromiko            = '#ffbc6b'
            LightMountainMeadow = '#16a766'
            LightShamrock       = '#43d692'
            LuxorGold           = '#aa8831'
            MandysPink          = '#f6c5be'
            MediumPurple        = '#a479e2'
            Meteorite           = '#41236d'
            MoonRaker           = '#d0bcf1'
            LightMoonRaker      = '#e4d7f5'
            MountainMeadow      = '#149e60'
            Oasis               = '#fef1d1'
            OceanGreen          = '#44b984'
            OldGold             = '#d5ae49'
            Perano              = '#a4c2f4'
            PersianPink         = '#f691b3'
            PigPink             = '#fcdee8'
            Pueblo              = '#822111'
            RedOrange           = '#fb4c2f'
            RoyalBlue           = '#3c78d8'
            RoyalPurple         = '#653e9b'
            Salem               = '#0b804b'
            Salomie             = '#fcda83'
            SeaPink             = '#efa093'
            Shamrock            = '#3dc789'
            Silver              = '#cccccc'
            Tabasco             = '#ac2b16'
            Tequila             = '#ffe6c7'
            Thunderbird         = '#cc3a21'
            TropicalBlue        = '#c9daf8'
            TulipTree           = '#eaa041'
            Tundora             = '#434343'
            VistaBlue           = '#89d3b2'
            Watercourse         = '#076239'
            WaterLeaf           = '#a0eac9'
            White               = '#ffffff'
            YellowOrange        = '#ffad47'
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        $body = New-Object 'Google.Apis.Gmail.v1.Data.Label'
        foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
            $body.$prop = $PSBoundParameters[$prop]
        }
        if ($PSBoundParameters.Keys -contains 'BackgroundColor' -or $PSBoundParameters.Keys -contains 'TextColor') {
            $color = New-Object 'Google.Apis.Gmail.v1.Data.LabelColor'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$color.PSObject.Properties.Name -contains $_}) {
                $color.$prop = $colorDict[$PSBoundParameters[$prop]]
            }
            $body.Color = $color
        }
        try {
            Write-Verbose "Creating Label '$Name' for user '$User'"
            $request = $service.Users.Labels.Create($body, $User)
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSGmailLabel'
function New-GSGmailSendAsAlias {
    <#
    .SYNOPSIS
    Creates a new SendAs alias for a user.
 
    .DESCRIPTION
    Creates a new SendAs alias for a user.
 
    .PARAMETER User
    The user to create the SendAs alias for.
 
    .PARAMETER SendAsEmail
    The email address that appears in the "From:" header for mail sent using this alias.
 
    .PARAMETER DisplayName
    A name that appears in the "From:" header for mail sent using this alias.
 
    For custom "from" addresses, when this is empty, Gmail will populate the "From:" header with the name that is used for the primary address associated with the account.
 
    If the admin has disabled the ability for users to update their name format, requests to update this field for the primary login will silently fail.
 
    .PARAMETER IsDefault
    Whether this address is selected as the default "From:" address in situations such as composing a new message or sending a vacation auto-reply.
 
    Every Gmail account has exactly one default send-as address, so the only legal value that clients may write to this field is true.
 
    Changing this from false to true for an address will result in this field becoming false for the other previous default address.
 
    .PARAMETER ReplyToAddress
    An optional email address that is included in a "Reply-To:" header for mail sent using this alias.
 
    If this is empty, Gmail will not generate a "Reply-To:" header.
 
    .PARAMETER Signature
    An optional HTML signature that is included in messages composed with this alias in the Gmail web UI.
 
    .PARAMETER SmtpMsa
    An optional SMTP service that will be used as an outbound relay for mail sent using this alias.
 
    If this is empty, outbound mail will be sent directly from Gmail's servers to the destination SMTP service. This setting only applies to custom "from" aliases.
 
    Use the helper function Add-GmailSmtpMsa to create the correct object for this parameter.
 
    .PARAMETER TreatAsAlias
    Whether Gmail should treat this address as an alias for the user's primary email address.
 
    This setting only applies to custom "from" aliases.
 
    .EXAMPLE
    $smtpMsa = Add-GSGmailSmtpMsa -Host 10.0.30.18 -Port 3770 -SecurityMode none -Username mailadmin -Password $(ConvertTo-SecureString $password -AsPlainText -Force)
    New-GSGmailSendAsAlias -SendAsEmail joseph.wiggum@business.com -User joe@domain.com -Signature "<div>Thank you for your time,</br>Joseph Wiggum</div>" -SmtpMsa $smtpMsa
 
    Creates a new SendAs alias for Joe's formal/work address including signature and SmtpMsa settings.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.SendAs')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User,
        [parameter(Mandatory = $true)]
        [string]
        $SendAsEmail,
        [parameter(Mandatory = $false)]
        [string]
        $DisplayName,
        [parameter(Mandatory = $false)]
        [switch]
        $IsDefault,
        [parameter(Mandatory = $false)]
        [string]
        $ReplyToAddress,
        [parameter(Mandatory = $false)]
        [string]
        $Signature,
        [parameter(Mandatory = $false)]
        [Google.Apis.Gmail.v1.Data.SmtpMsa]
        $SmtpMsa,
        [parameter(Mandatory = $false)]
        [switch]
        $TreatAsAlias
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        if (-not $PSBoundParameters.ContainsKey('SendAsEmail')) {
            $SendAsEmail = $User
        }
        elseif ($SendAsEmail -notlike "*@*.*") {
            $SendAsEmail = "$($SendAsEmail)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = @(
                'https://www.googleapis.com/auth/gmail.settings.sharing'
            )
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Gmail.v1.Data.SendAs'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                $body.$prop = $PSBoundParameters[$prop]
            }
            $request = $service.Users.Settings.SendAs.Create($body,$User)
            Write-Verbose "Creating new SendAs alias '$SendAsEmail' for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSGmailSendAsAlias'
function New-GSGmailSMIMEInfo {
    <#
    .SYNOPSIS
    Adds Gmail S/MIME info
 
    .DESCRIPTION
    Adds Gmail S/MIME info
 
    .PARAMETER SendAsEmail
    The email address that appears in the "From:" header for mail sent using this alias.
 
    .PARAMETER Pkcs12
    PKCS#12 format containing a single private/public key pair and certificate chain. This format is only accepted from client for creating a new SmimeInfo and is never returned, because the private key is not intended to be exported. PKCS#12 may be encrypted, in which case encryptedKeyPassword should be set appropriately.
 
    .PARAMETER EncryptedKeyPassword
    Encrypted key password, when key is encrypted.
 
    .PARAMETER IsDefault
    Whether this SmimeInfo is the default one for this user's send-as address.
 
    .PARAMETER User
    The user's email address
 
    .EXAMPLE
    New-GSGmailSMIMEInfo -SendAsEmail 'joe@otherdomain.com' -Pkcs12 .\MyCert.pfx -User joe@domain.com
 
    Creates a specified S/MIME for Joe's SendAsEmail 'joe@otherdomain.com' using the provided PKCS12 certificate
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.SmimeInfo')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $SendAsEmail,
        [parameter(Mandatory = $true)]
        [ValidateScript( {
                if (!(Test-Path $_)) {
                    throw "Please enter a valid file path."
                }
                elseif ($_ -notlike "*.pfx" -and $_ -notlike "*.p12") {
                    throw "Pkcs12 must be a .pfx or .p12 file"
                }
                else {
                    $true
                }
            })]
        [string]
        $Pkcs12,
        [parameter(Mandatory = $false)]
        [SecureString]
        $EncryptedKeyPassword,
        [parameter(Mandatory = $false)]
        [Switch]
        $IsDefault,
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User
    )
    Process {
        Resolve-Email ([Ref]$User)
        $serviceParams = @{
            Scope       = @(
                'https://www.googleapis.com/auth/gmail.settings.basic'
                'https://www.googleapis.com/auth/gmail.settings.sharing'
            )
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Gmail.v1.Data.SmimeInfo'
            foreach ($key in $PSBoundParameters.Keys | Where-Object { $body.PSObject.Properties.Name -contains $_ }) {
                switch ($key) {
                    EncryptedKeyPassword {
                        $pw = (New-Object PSCredential "user", $PSBoundParameters[$key]).GetNetworkCredential().Password
                        $body.EncryptedKeyPassword = $pw
                    }
                    Pkcs12 {
                        $pkcs12Content = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Pkcs12))
                        $urlSafePkcs12Content = $pkcs12Content.Replace('+', '-').Replace('/', '_')
                        $body.Pkcs12 = $urlSafePkcs12Content
                    }
                    Default {
                        $body.$key = $PSBoundParameters[$key]
                    }
                }
            }
            Write-Verbose "Adding new S/MIME of SendAsEmail '$SendAsEmail' for user '$User' using Certificate '$Pkcs12'"
            $request = $service.Users.Settings.SendAs.SmimeInfo.Insert($body, $User, $SendAsEmail)
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSGmailSMIMEInfo'
function Remove-GSGmailFilter {
    <#
    .SYNOPSIS
    Removes a Gmail filter
 
    .DESCRIPTION
    Removes a Gmail filter
 
    .PARAMETER User
    The primary email of the user to remove the filter from
 
    Defaults to the AdminEmail user
 
    .PARAMETER FilterId
    The unique Id of the filter to remove
 
    .PARAMETER Raw
    If $true, returns the raw response. If not passed or -Raw:$false, response is formatted as a flat object for readability
 
    .EXAMPLE
    Remove-GSGmailFilter -FilterId ANe1Bmj5l3089jd3k1eQbY90g9rXswjS03LVOw
 
    Removes the Filter from the AdminEmail user after confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("Id")]
        [string[]]
        $FilterId,
        [parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            foreach ($fil in $FilterId) {
                if ($PSCmdlet.ShouldProcess("Deleting Filter Id '$fil' from user '$User'")) {
                    Write-Verbose "Deleting Filter Id '$fil' from user '$User'"
                    $request = $service.Users.Settings.Filters.Delete($User,$fil)
                    $request.Execute()
                    Write-Verbose "Filter Id '$fil' deleted successfully from user '$User'"
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGmailFilter'
function Remove-GSGmailLabel {
    <#
    .SYNOPSIS
    Removes a Gmail label
 
    .DESCRIPTION
    Removes a Gmail label
 
    .PARAMETER LabelId
    The unique Id of the label to remove
 
    .PARAMETER User
    The primary email of the user to remove the label from
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Remove-GSGmailLabel -LabelId ANe1Bmj5l3089jd3k1eQbY90g9rXswjS03LVOw
 
    Removes the Label from the AdminEmail user after confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Alias("Id")]
        [string[]]
        $LabelId,
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($label in $LabelId) {
            try {
                if ($PSCmdlet.ShouldProcess("Deleting Label Id '$label' from user '$User'")) {
                    Write-Verbose "Deleting Label Id '$label' from user '$User'"
                    $request = $service.Users.Labels.Delete($User, $label)
                    $request.Execute()
                    Write-Verbose "Label Id '$label' deleted successfully from user '$User'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGmailLabel'
function Remove-GSGmailMessage {
    <#
    .SYNOPSIS
    Removes a Gmail message from the user
 
    .DESCRIPTION
    Removes a Gmail message from the user
 
    .PARAMETER Id
    The Id of the message to remove
 
    .PARAMETER Filter
    The Gmail query to pull the list of messages to remove instead of passing the MessageId directly
 
    .PARAMETER MaxToModify
    The maximum amount of emails you would like to remove. Use this with the `Filter` parameter as a safeguard.
 
    .PARAMETER Method
    The method used to delete the message
 
    Available values are:
    * "Trash": moves the message to the TRASH label (Default - preferred method, as this is recoverable)
    * "Delete": permanently deletes the message (NON-RECOVERABLE!)
 
    Default value is 'Trash'
 
    .PARAMETER User
    The primary email of the user to remove the message from
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Remove-GSGmailMessage -User joe -Id 161622d7b76b7e1e,1616227c34d435f2
 
    Moves the 2 message Id's from Joe's inbox into their TRASH after confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High",DefaultParameterSetName = "MessageId")]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "MessageId")]
        [Alias('MessageId')]
        [String[]]
        $Id,
        [parameter(Mandatory = $true, ParameterSetName = "Filter")]
        [Alias('Query')]
        [string]
        $Filter,
        [parameter(Mandatory = $false,ParameterSetName = "Filter")]
        [int]
        $MaxToModify,
        [parameter(Mandatory = $false)]
        [ValidateSet('Trash','Delete')]
        [String]
        $Method = 'Trash',
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($MyInvocation.InvocationName -eq 'Move-GSGmailMessageToTrash') {
            $Method = 'Trash'
        }
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $msgId = switch ($PSCmdlet.ParameterSetName) {
            MessageId {
                $Id
            }
            Filter {
                (Get-GSGmailMessageList -Filter $Filter -User $User).Id
            }
        }
        if ($PSBoundParameters.Keys -contains 'MaxToModify' -and $msgId.Count -gt $MaxToModify) {
            Write-Error "MaxToModify is set to $MaxToModify but total modifications are $($msgId.Count). No action taken."
        }
        else {
            $service = New-GoogleService @serviceParams
            try {
                foreach ($mId in $msgId) {
                    $request = switch ($Method) {
                        Trash {
                            $service.Users.Messages.Trash($User,$mId)
                            $message = "moved to TRASH"
                        }
                        Delete {
                            $service.Users.Messages.Delete($User,$mId)
                            $message = "deleted"
                        }
                    }
                    if ($PSCmdlet.ShouldProcess("Removing Message Id '$mId' for user '$User'")) {
                        Write-Verbose "Removing Message Id '$mId' for user '$User'"
                        $res = $request.Execute()
                        if ($res) {
                            $res | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                        }
                        Write-Verbose "Message ID '$mId' successfully $message for user '$User'"
                    }
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGmailMessage'
function Remove-GSGmailSendAsAlias {
    <#
    .SYNOPSIS
    Removes a Gmail SendAs alias.
 
    .DESCRIPTION
    Removes a Gmail SendAs alias.
 
    .PARAMETER SendAsEmail
    The SendAs alias to be removed.
 
    .PARAMETER User
    The email of the user you are removing the SendAs alias from.
 
    .EXAMPLE
    Remove-GSGmailSendAsAlias -SendAsEmail partyfuntime@domain.com -User joe@domain.com
 
    Remove Joe's fun custom Sendas alias that he had created in the early days of the company :-(
    #>

    [OutputType()]
    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("SendAs")]
        [string[]]
        $SendAsEmail,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.sharing'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($sendAs in $SendAsEmail) {
            try {
                if ($sendAs -notlike "*@*.*") {
                    $sendAs = "$($sendAs)@$($Script:PSGSuite.Domain)"
                }
                if ($PSCmdlet.ShouldProcess("Removing SendAs alias '$sendAs' from user '$User'")) {
                    Write-Verbose "Removing SendAs alias '$sendAs' from user '$User'"
                    $request = $service.Users.Settings.SendAs.Delete($User,$sendAs)
                    $request.Execute()
                    Write-Verbose "SendAs alias '$sendAs' successfully removed from user '$User'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGmailSendAsAlias'
function Remove-GSGmailSMIMEInfo {
    <#
    .SYNOPSIS
    Removes Gmail S/MIME info
 
    .DESCRIPTION
    Removes Gmail S/MIME info
 
    .PARAMETER SendAsEmail
    The email address that appears in the "From:" header for mail sent using this alias.
 
    .PARAMETER Id
    The immutable ID for the SmimeInfo
 
    .PARAMETER User
    The user's email address
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Remove-GSGmailSMIMEInfo -SendAsEmail 'joe@otherdomain.com' -Id 1008396210820120578939 -User joe@domain.com
 
    Removes the specified S/MIME info for Joe's SendAsEmail 'joe@otherdomain.com'.
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [string]
        $SendAsEmail,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Id,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($I in $Id) {
            try {
                if ($PSCmdlet.ShouldProcess("Removing S/MIME Id '$I' of SendAsEmail '$SendAsEmail' for user '$User'")) {
                    $request = $service.Users.Settings.SendAs.SmimeInfo.Delete($User,$SendAsEmail,$I)
                    $request.Execute()
                    Write-Verbose "Successfully removed S/MIME Id '$I' of SendAsEmail '$SendAsEmail' for user '$User'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGmailSMIMEInfo'
function Restore-GSGmailMessage {
    <#
    .SYNOPSIS
    Restores a trashed message to the inbox
 
    .DESCRIPTION
    Restores a trashed message to the inbox
 
    .PARAMETER User
    The primary email of the user to restore the message for
 
    Defaults to the AdminEmail user
 
    .PARAMETER Id
    The Id of the message to restore
 
    .EXAMPLE
    Restore-GSGmailMessage -User joe -Id 161622d7b76b7e1e,1616227c34d435f2
 
    Restores the 2 message Id's from Joe's TRASH back to their inbox
    #>

    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('MessageID')]
        [String[]]
        $Id
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            foreach ($mId in $Id) {
                $request = $service.Users.Messages.Untrash($User,$mId)
                Write-Verbose "Removing Message Id '$mId' from TRASH for user '$User'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Restore-GSGmailMessage'
function Send-GmailMessage {
    <#
    .SYNOPSIS
    Sends a Gmail message
 
    .DESCRIPTION
    Sends a Gmail message. Designed for parity with Send-GmailMessage
 
    .PARAMETER From
    The primary email of the user that is sending the message. This MUST be a user account owned by the customer, as the Gmail Service must be built under this user's context and will fail if a group or alias is passed instead
 
    Defaults to the AdminEmail user
 
    .PARAMETER Subject
    The subject of the email
 
    .PARAMETER Body
    The email body. Supports HTML when used in conjunction with the -BodyAsHtml parameter
 
    .PARAMETER To
    The To recipient(s) of the email
 
    .PARAMETER CC
    The Cc recipient(s) of the email
 
    .PARAMETER BCC
    The Bcc recipient(s) of the email
 
    .PARAMETER Attachments
    The attachment(s) of the email
 
    .PARAMETER BodyAsHtml
    If passed, renders the HTML content of the body on send
 
    .EXAMPLE
    Send-GmailMessage -From Joe -To john.doe@domain.com -Subject "New Pricing Models" -Body $body -BodyAsHtml -Attachments 'C:\Reports\PricingModel_2018.xlsx'
 
    Sends a message from Joe to john.doe@domain.com with HTML body and an Excel spreadsheet attached
    #>

    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false)]
        [Alias("PrimaryEmail","UserKey","Mail","User")]
        [ValidateNotNullOrEmpty()]
        [String]
        $From = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $true)]
        [string]
        $Subject,
        [parameter(Mandatory = $false)]
        [string]
        $Body,
        [parameter(Mandatory = $false)]
        [string[]]
        $To,
        [parameter(Mandatory = $false)]
        [string[]]
        $CC,
        [parameter(Mandatory = $false)]
        [string[]]
        $BCC,
        [parameter(Mandatory = $false)]
        [ValidateScript( {Test-Path $_})]
        [string[]]
        $Attachments,
        [parameter(Mandatory = $false)]
        [switch]
        $BodyAsHtml
    )
    Process {
        $User = $From -replace ".*<","" -replace ">",""
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
            $From = $User
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
            $From = $User
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        $messageParams = @{
            From                     = $From
            Subject                  = $Subject
            ReturnConstructedMessage = $true
        }
        if ($To) {
            $messageParams.Add("To",@($To))
        }
        if ($Body) {
            $messageParams.Add("Body",$Body)
        }
        if ($CC) {
            $messageParams.Add("CC",@($CC))
        }
        if ($BCC) {
            $messageParams.Add("BCC",@($BCC))
        }
        if ($Attachments) {
            $messageParams.Add("Attachment",@($Attachments))
        }
        if ($BodyAsHtml) {
            $messageParams.Add("BodyAsHtml",$true)
        }
        $raw = New-MimeMessage @messageParams | Convert-Base64 -From NormalString -To WebSafeBase64String
        try {
            $bodySend = New-Object 'Google.Apis.Gmail.v1.Data.Message' -Property @{
                Raw = $raw
            }
            $request = $service.Users.Messages.Send($bodySend,$User)
            Write-Verbose "Sending Message '$Subject' from user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Send-GmailMessage'
function Send-GSGmailSendAsConfirmation {
    <#
    .SYNOPSIS
    Sends a verification email to the specified send-as alias address. The verification status must be pending.
 
    .DESCRIPTION
    Sends a verification email to the specified send-as alias address. The verification status must be pending.
 
    .PARAMETER SendAsEmail
    The SendAs alias to be verified.
 
    .PARAMETER User
    The email of the user you are verifying the SendAs alias for.
 
    .EXAMPLE
    Send-GSGmailSendAsConfirmation -SendAsEmail joseph.wiggum@work.com -User joe@domain.com
 
    Sends a verification email to Joe's work address to confirm joe@domain.com as being able to send-as that account.
    #>

    [OutputType()]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("SendAs")]
        [string[]]
        $SendAsEmail,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.sharing'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        foreach ($sendAs in $SendAsEmail) {
            try {
                if ($sendAs -notlike "*@*.*") {
                    $sendAs = "$($sendAs)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Sending verification email to SendAs alias '$sendAs' for user '$User'"
                $request = $service.Users.Settings.SendAs.Verify($User,$sendAs)
                $request.Execute()
                Write-Verbose "Verification email for SendAs alias '$sendAs' of user '$User' sent successfully"
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Send-GSGmailSendAsConfirmation'
function Update-GSGmailAutoForwardingSettings {
    <#
    .SYNOPSIS
    Updates the auto-forwarding setting for the specified account. A verified forwarding address must be specified when auto-forwarding is enabled.
 
    .DESCRIPTION
    Updates the auto-forwarding setting for the specified account. A verified forwarding address must be specified when auto-forwarding is enabled.
 
    .PARAMETER User
    The user to update the AutoForwarding settings for
 
    .PARAMETER Disposition
    The state that a message should be left in after it has been forwarded.
 
    Acceptable values are:
    * "archive": Archive the message.
    * "dispositionUnspecified": Unspecified disposition.
    * "leaveInInbox": Leave the message in the INBOX.
    * "markRead": Leave the message in the INBOX and mark it as read.
    * "trash": Move the message to the TRASH.
 
    .PARAMETER EmailAddress
    Email address to which all incoming messages are forwarded. This email address must be a verified member of the forwarding addresses.
 
    .PARAMETER Enabled
    Whether all incoming mail is automatically forwarded to another address.
 
    .EXAMPLE
    Update-GSGmailAutoForwardingSettings -User me -Disposition leaveInInbox -EmailAddress joe@domain.com -Enabled
 
    Enables auto forwarding of all mail for the AdminEmail user. Forwarded mail will be left in their inbox.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.AutoForwarding')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User,
        [parameter(Mandatory = $false)]
        [ValidateSet('archive','dispositionUnspecified','leaveInInbox','markRead','trash')]
        [string]
        $Disposition,
        [parameter(Mandatory = $false)]
        [string]
        $EmailAddress,
        [parameter(Mandatory = $false)]
        [switch]
        $Enabled
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.sharing'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            $body = New-Object 'Google.Apis.Gmail.v1.Data.AutoForwarding'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                $body.$prop = $PSBoundParameters[$prop]
            }
            $request = $service.Users.Settings.UpdateAutoForwarding($body,$User)
            Write-Verbose "Updating AutoForwarding settings for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailAutoForwardingSettings'
function Update-GSGmailImapSettings {
    <#
    .SYNOPSIS
    Updates IMAP settings
 
    .DESCRIPTION
    Updates IMAP settings
 
    .PARAMETER User
    The user to update the IMAP settings for
 
    .PARAMETER AutoExpunge
    If this value is true, Gmail will immediately expunge a message when it is marked as deleted in IMAP. Otherwise, Gmail will wait for an update from the client before expunging messages marked as deleted.
 
    .PARAMETER Enabled
    Whether IMAP is enabled for the account.
 
    .PARAMETER ExpungeBehavior
    The action that will be executed on a message when it is marked as deleted and expunged from the last visible IMAP folder.
 
    Acceptable values are:
    * "archive": Archive messages marked as deleted.
    * "deleteForever": Immediately and permanently delete messages marked as deleted. The expunged messages cannot be recovered.
    * "expungeBehaviorUnspecified": Unspecified behavior.
    * "trash": Move messages marked as deleted to the trash.
 
    .PARAMETER MaxFolderSize
    An optional limit on the number of messages that an IMAP folder may contain. Legal values are 0, 1000, 2000, 5000 or 10000. A value of zero is interpreted to mean that there is no limit.
 
    .EXAMPLE
    Update-GSGmailImapSettings -Enabled:$false -User me
 
    Disables IMAP for the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.ImapSettings')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User,
        [parameter(Mandatory = $false)]
        [switch]
        $AutoExpunge,
        [parameter(Mandatory = $false)]
        [switch]
        $Enabled,
        [parameter(Mandatory = $false)]
        [ValidateSet('archive','deleteForever','expungeBehaviorUnspecified','trash')]
        [string]
        $ExpungeBehavior,
        [parameter(Mandatory = $false)]
        [ValidateSet(0,1000,2000,5000,10000)]
        [int]
        $MaxFolderSize
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            $body = New-Object 'Google.Apis.Gmail.v1.Data.ImapSettings'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                $body.$prop = $PSBoundParameters[$prop]
            }
            $request = $service.Users.Settings.UpdateImap($body,$User)
            Write-Verbose "Updating IMAP settings for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailImapSettings'
function Update-GSGmailLabel {
    <#
    .SYNOPSIS
    Updates Gmail label information for the specified labelid
 
    .DESCRIPTION
    Updates Gmail label information for the specified labelid
 
    .PARAMETER LabelId
    The unique Id of the label to update
 
    .PARAMETER LabelListVisibility
    The visibility of the label in the label list in the Gmail web interface.
 
    Acceptable values are:
    * "labelHide": Do not show the label in the label list.
    * "labelShow": Show the label in the label list. (Default)
    * "labelShowIfUnread": Show the label if there are any unread messages with that label.
 
    .PARAMETER MessageListVisibility
    The visibility of messages with this label in the message list in the Gmail web interface.
 
    Acceptable values are:
    * "hide": Do not show the label in the message list.
    * "show": Show the label in the message list. (Default)
 
    .PARAMETER Name
    The display name of the label
 
    .PARAMETER BackgroundColor
    The background color of the label
 
    Options and their corresponding hex code:
 
        Amethyst = '#8e63ce'
        BananaMania = '#fce8b3'
        Bermuda = '#68dfa9'
        BilobaFlower = '#b694e8'
        Black = '#000000'
        BlueRomance = '#c6f3de'
        BrandyPunch = '#cf8933'
        BurntSienna = '#e66550'
        Cadillac = '#b65775'
        Camelot = '#83334c'
        CeruleanBlue = '#285bac'
        ChathamsBlue = '#1c4587'
        Concrete = '#f3f3f3'
        CornflowerBlue = '#4a86e8'
        LightCornflowerBlue = '#6d9eeb'
        CreamCan = '#f2c960'
        Cupid = '#fbc8d9'
        DeepBlush = '#e07798'
        Desert = '#a46a21'
        DoveGray = '#666666'
        DustyGray = '#999999'
        Eucalyptus = '#2a9c68'
        Flesh = '#ffd6a2'
        FringyFlower = '#b9e4d0'
        Gallery = '#efefef'
        Goldenrod = '#fad165'
        Illusion = '#f7a7c0'
        Jewel = '#1a764d'
        Koromiko = '#ffbc6b'
        LightMountainMeadow = '#16a766'
        LightShamrock = '#43d692'
        LuxorGold = '#aa8831'
        MandysPink = '#f6c5be'
        MediumPurple = '#a479e2'
        Meteorite = '#41236d'
        MoonRaker = '#d0bcf1'
        LightMoonRaker = '#e4d7f5'
        MountainMeadow = '#149e60'
        Oasis = '#fef1d1'
        OceanGreen = '#44b984'
        OldGold = '#d5ae49'
        Perano = '#a4c2f4'
        PersianPink = '#f691b3'
        PigPink = '#fcdee8'
        Pueblo = '#822111'
        RedOrange = '#fb4c2f'
        RoyalBlue = '#3c78d8'
        RoyalPurple = '#653e9b'
        Salem = '#0b804b'
        Salomie = '#fcda83'
        SeaPink = '#efa093'
        Shamrock = '#3dc789'
        Silver = '#cccccc'
        Tabasco = '#ac2b16'
        Tequila = '#ffe6c7'
        Thunderbird = '#cc3a21'
        TropicalBlue = '#c9daf8'
        TulipTree = '#eaa041'
        Tundora = '#434343'
        VistaBlue = '#89d3b2'
        Watercourse = '#076239'
        WaterLeaf = '#a0eac9'
        White = '#ffffff'
        YellowOrange = '#ffad47'
 
    .PARAMETER TextColor
    The text color of the label
 
    Options and their corresponding hex code:
 
        Amethyst = '#8e63ce'
        BananaMania = '#fce8b3'
        Bermuda = '#68dfa9'
        BilobaFlower = '#b694e8'
        Black = '#000000'
        BlueRomance = '#c6f3de'
        BrandyPunch = '#cf8933'
        BurntSienna = '#e66550'
        Cadillac = '#b65775'
        Camelot = '#83334c'
        CeruleanBlue = '#285bac'
        ChathamsBlue = '#1c4587'
        Concrete = '#f3f3f3'
        CornflowerBlue = '#4a86e8'
        LightCornflowerBlue = '#6d9eeb'
        CreamCan = '#f2c960'
        Cupid = '#fbc8d9'
        DeepBlush = '#e07798'
        Desert = '#a46a21'
        DoveGray = '#666666'
        DustyGray = '#999999'
        Eucalyptus = '#2a9c68'
        Flesh = '#ffd6a2'
        FringyFlower = '#b9e4d0'
        Gallery = '#efefef'
        Goldenrod = '#fad165'
        Illusion = '#f7a7c0'
        Jewel = '#1a764d'
        Koromiko = '#ffbc6b'
        LightMountainMeadow = '#16a766'
        LightShamrock = '#43d692'
        LuxorGold = '#aa8831'
        MandysPink = '#f6c5be'
        MediumPurple = '#a479e2'
        Meteorite = '#41236d'
        MoonRaker = '#d0bcf1'
        LightMoonRaker = '#e4d7f5'
        MountainMeadow = '#149e60'
        Oasis = '#fef1d1'
        OceanGreen = '#44b984'
        OldGold = '#d5ae49'
        Perano = '#a4c2f4'
        PersianPink = '#f691b3'
        PigPink = '#fcdee8'
        Pueblo = '#822111'
        RedOrange = '#fb4c2f'
        RoyalBlue = '#3c78d8'
        RoyalPurple = '#653e9b'
        Salem = '#0b804b'
        Salomie = '#fcda83'
        SeaPink = '#efa093'
        Shamrock = '#3dc789'
        Silver = '#cccccc'
        Tabasco = '#ac2b16'
        Tequila = '#ffe6c7'
        Thunderbird = '#cc3a21'
        TropicalBlue = '#c9daf8'
        TulipTree = '#eaa041'
        Tundora = '#434343'
        VistaBlue = '#89d3b2'
        Watercourse = '#076239'
        WaterLeaf = '#a0eac9'
        White = '#ffffff'
        YellowOrange = '#ffad47'
 
    .PARAMETER User
    The user to update label information for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Update-GSGmailLabel -User user@domain.com -LabelId Label_79 -BackgroundColor Black -TextColor Bermuda
 
    Updates the specified Gmail label with new background and text colors
 
    .EXAMPLE
    Get-GSGmailLabel | Where-Object {$_.LabelListVisibility -eq 'labelShowIfUnread'} | Update-GSGmailLabel -LabelListVisibility labelShow -BackgroundColor Bermuda -TextColor Tundora
 
    Updates all labels with LabelListVisibility of 'labelShowIfUnread' with new background and text colors and sets all of them to always show
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.Label')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Alias("Id")]
        [string[]]
        $LabelId,
        [parameter(Mandatory = $false, Position = 1)]
        [ValidateSet("labelHide","labelShow","labelShowIfUnread")]
        [string]
        $LabelListVisibility,
        [parameter(Mandatory = $false, Position = 2)]
        [ValidateSet("hide","show")]
        [string]
        $MessageListVisibility,
        [parameter(Mandatory = $false, Position = 3)]
        [string]
        $Name,
        [parameter(Mandatory = $false)]
        [ValidateSet('Amethyst','BananaMania','Bermuda','BilobaFlower','Black','BlueRomance','BrandyPunch','BurntSienna','Cadillac','Camelot','CeruleanBlue','ChathamsBlue','Concrete','CornflowerBlue','CreamCan','Cupid','DeepBlush','Desert','DoveGray','DustyGray','Eucalyptus','Flesh','FringyFlower','Gallery','Goldenrod','Illusion','Jewel','Koromiko','LightCornflowerBlue','LightMoonRaker','LightMountainMeadow','LightShamrock','LuxorGold','MandysPink','MediumPurple','Meteorite','MoonRaker','MountainMeadow','Oasis','OceanGreen','OldGold','Perano','PersianPink','PigPink','Pueblo','RedOrange','RoyalBlue','RoyalPurple','Salem','Salomie','SeaPink','Shamrock','Silver','Tabasco','Tequila','Thunderbird','TropicalBlue','TulipTree','Tundora','VistaBlue','Watercourse','WaterLeaf','White','YellowOrange')]
        [string]
        $BackgroundColor,
        [parameter(Mandatory = $false)]
        [ValidateSet('Amethyst','BananaMania','Bermuda','BilobaFlower','Black','BlueRomance','BrandyPunch','BurntSienna','Cadillac','Camelot','CeruleanBlue','ChathamsBlue','Concrete','CornflowerBlue','CreamCan','Cupid','DeepBlush','Desert','DoveGray','DustyGray','Eucalyptus','Flesh','FringyFlower','Gallery','Goldenrod','Illusion','Jewel','Koromiko','LightCornflowerBlue','LightMoonRaker','LightMountainMeadow','LightShamrock','LuxorGold','MandysPink','MediumPurple','Meteorite','MoonRaker','MountainMeadow','Oasis','OceanGreen','OldGold','Perano','PersianPink','PigPink','Pueblo','RedOrange','RoyalBlue','RoyalPurple','Salem','Salomie','SeaPink','Shamrock','Silver','Tabasco','Tequila','Thunderbird','TropicalBlue','TulipTree','Tundora','VistaBlue','Watercourse','WaterLeaf','White','YellowOrange')]
        [string]
        $TextColor,
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $colorDict = @{
            Amethyst            = '#8e63ce'
            BananaMania         = '#fce8b3'
            Bermuda             = '#68dfa9'
            BilobaFlower        = '#b694e8'
            Black               = '#000000'
            BlueRomance         = '#c6f3de'
            BrandyPunch         = '#cf8933'
            BurntSienna         = '#e66550'
            Cadillac            = '#b65775'
            Camelot             = '#83334c'
            CeruleanBlue        = '#285bac'
            ChathamsBlue        = '#1c4587'
            Concrete            = '#f3f3f3'
            CornflowerBlue      = '#4a86e8'
            LightCornflowerBlue = '#6d9eeb'
            CreamCan            = '#f2c960'
            Cupid               = '#fbc8d9'
            DeepBlush           = '#e07798'
            Desert              = '#a46a21'
            DoveGray            = '#666666'
            DustyGray           = '#999999'
            Eucalyptus          = '#2a9c68'
            Flesh               = '#ffd6a2'
            FringyFlower        = '#b9e4d0'
            Gallery             = '#efefef'
            Goldenrod           = '#fad165'
            Illusion            = '#f7a7c0'
            Jewel               = '#1a764d'
            Koromiko            = '#ffbc6b'
            LightMountainMeadow = '#16a766'
            LightShamrock       = '#43d692'
            LuxorGold           = '#aa8831'
            MandysPink          = '#f6c5be'
            MediumPurple        = '#a479e2'
            Meteorite           = '#41236d'
            MoonRaker           = '#d0bcf1'
            LightMoonRaker      = '#e4d7f5'
            MountainMeadow      = '#149e60'
            Oasis               = '#fef1d1'
            OceanGreen          = '#44b984'
            OldGold             = '#d5ae49'
            Perano              = '#a4c2f4'
            PersianPink         = '#f691b3'
            PigPink             = '#fcdee8'
            Pueblo              = '#822111'
            RedOrange           = '#fb4c2f'
            RoyalBlue           = '#3c78d8'
            RoyalPurple         = '#653e9b'
            Salem               = '#0b804b'
            Salomie             = '#fcda83'
            SeaPink             = '#efa093'
            Shamrock            = '#3dc789'
            Silver              = '#cccccc'
            Tabasco             = '#ac2b16'
            Tequila             = '#ffe6c7'
            Thunderbird         = '#cc3a21'
            TropicalBlue        = '#c9daf8'
            TulipTree           = '#eaa041'
            Tundora             = '#434343'
            VistaBlue           = '#89d3b2'
            Watercourse         = '#076239'
            WaterLeaf           = '#a0eac9'
            White               = '#ffffff'
            YellowOrange        = '#ffad47'
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        $body = New-Object 'Google.Apis.Gmail.v1.Data.Label'
        foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
            $body.$prop = $PSBoundParameters[$prop]
        }
        if ($PSBoundParameters.Keys -contains 'BackgroundColor' -or $PSBoundParameters.Keys -contains 'TextColor') {
            $color = New-Object 'Google.Apis.Gmail.v1.Data.LabelColor'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$color.PSObject.Properties.Name -contains $_}) {
                $color.$prop = $colorDict[$PSBoundParameters[$prop]]
            }
            $body.Color = $color
        }
        foreach ($label in $LabelId) {
            try {
                Write-Verbose "Updating Label Id '$label' for user '$User'"
                $request = $service.Users.Labels.Patch($body, $User, $label)
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailLabel'
function Update-GSGmailLanguageSettings {
    <#
    .SYNOPSIS
    Updates Gmail display language settings
 
    .DESCRIPTION
    Updates Gmail display language settings
 
    .PARAMETER User
    The user to update the Gmail display language settings for
 
    .PARAMETER Language
    The language to display Gmail in, formatted as an RFC 3066 Language Tag (for example en-GB, fr or ja for British English, French, or Japanese respectively).
 
    The set of languages supported by Gmail evolves over time, so please refer to the "Language" dropdown in the Gmail settings for all available options, as described in the language settings help article. A table of sample values is also provided in the Managing Language Settings guide
 
    Not all Gmail clients can display the same set of languages. In the case that a user's display language is not available for use on a particular client, said client automatically chooses to display in the closest supported variant (or a reasonable default).
 
    .EXAMPLE
    Update-GSGmailLanguageSettings -User me -Language fr
 
    Updates the Gmail display language to French for the AdminEmail user.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.LanguageSettings')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $User,
        [parameter(Mandatory,Position = 1)]
        [ValidateSet('af','am','ar','az','bg','bn','ca','chr','cs','cy','da','de','el','en','en-GB','es','es-419','et','eu','fa','fi','fil','fr','fr-CA','ga','gl','gu','he','hi','hr','hu','hy','id','is','it','ja','ka','km','kn','ko','lo','lt','lv','ml','mn','mr','ms','my','ne','nl','no','pl','pt-BR','pt-PT','ro','ru','si','sk','sl','sr','sv','sw','ta','te','th','tr','uk','ur','vi','zh-CN','zh-HK','zh-TW','zu')]
        [string]
        $Language
    )
    Process {
        foreach ($U in $User) {
            if ($U -ceq 'me') {
                $U = $Script:PSGSuite.AdminEmail
            }
            elseif ($U -notlike "*@*.*") {
                $U = "$($U)@$($Script:PSGSuite.Domain)"
            }
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
                ServiceType = 'Google.Apis.Gmail.v1.GmailService'
                User        = $U
            }
            $service = New-GoogleService @serviceParams
            try {
                $body = New-Object 'Google.Apis.Gmail.v1.Data.LanguageSettings' -Property @{
                    DisplayLanguage = $Language
                }
                $request = $service.Users.Settings.UpdateLanguage($body,$U)
                Write-Verbose "Updating Gmail Display Language for user '$U' to '$Language'"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailLanguageSettings'
function Update-GSGmailMessageLabels {
    <#
    .SYNOPSIS
    Updates Gmail label information for the specified message
 
    .DESCRIPTION
    Updates Gmail label information for the specified message
 
    .PARAMETER MessageId
    The unique Id of the message to update.
 
    .PARAMETER Filter
    The Gmail query to pull the list of messages to update instead of passing the MessageId directly.
 
    .PARAMETER MaxToModify
    The maximum amount of emails you would like to remove. Use this with the `Filter` parameter as a safeguard.
 
    .PARAMETER AddLabel
    The label(s) to add to the message. This supports either the unique LabelId or the Display Name for the label
 
    .PARAMETER RemoveLabel
    The label(s) to remove from the message. This supports either the unique LabelId or the Display Name for the label
 
    .PARAMETER User
    The user to update message labels for
 
    Defaults to the AdminEmail user
 
    .EXAMPLE
    Set-GSGmailLabel -user user@domain.com -LabelId Label_798170282134616520 -
 
    Gets the Gmail labels of the AdminEmail user
    #>

    [cmdletbinding(DefaultParameterSetName = "MessageId")]
    Param
    (
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "MessageId")]
        [Alias("Id")]
        [string[]]
        $MessageId,
        [parameter(Mandatory = $true, ParameterSetName = "Filter")]
        [Alias('Query')]
        [string]
        $Filter,
        [parameter(Mandatory = $false,ParameterSetName = "Filter")]
        [int]
        $MaxToModify,
        [parameter(Mandatory = $false)]
        [string[]]
        $AddLabel,
        [parameter(Mandatory = $false)]
        [string[]]
        $RemoveLabel,
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User = $Script:PSGSuite.AdminEmail
    )
    Process {
        if ($PSBoundParameters.Keys -notcontains 'AddLabel' -and $PSBoundParameters.Keys -notcontains 'RemoveLabel') {
            throw "You must specify a value for either AddLabel or RemoveLabel!"
        }
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://mail.google.com'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $msgId = switch ($PSCmdlet.ParameterSetName) {
            MessageId {
                $MessageId
            }
            Filter {
                (Get-GSGmailMessageList -Filter $Filter -User $User).Id
            }
        }
        if ($PSBoundParameters.Keys -contains 'MaxToModify' -and $msgId.Count -gt $MaxToModify) {
            Write-Error "MaxToModify is set to $MaxToModify but total modifications are $($msgId.Count). No action taken."
        }
        else {
            $service = New-GoogleService @serviceParams
            $userLabels = @{}
            Get-GSGmailLabel -User $User -Verbose:$false | ForEach-Object {
                $userLabels[$_.Name] = $_.Id
            }
            $body = New-Object 'Google.Apis.Gmail.v1.Data.ModifyMessageRequest'
            if ($PSBoundParameters.Keys -contains 'AddLabel') {
                $addLs = New-Object 'System.Collections.Generic.List[System.String]'
                foreach ($label in $AddLabel) {
                    try {
                        $addLs.Add($userLabels[$label])
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
                $body.AddLabelIds = $addLs
            }
            if ($PSBoundParameters.Keys -contains 'RemoveLabel') {
                $remLs = New-Object 'System.Collections.Generic.List[System.String]'
                foreach ($label in $RemoveLabel) {
                    try {
                        $remLs.Add($userLabels[$label])
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
                $body.RemoveLabelIds = $remLs
            }
            foreach ($message in $msgId) {
                try {
                    $request = $service.Users.Messages.Modify($body, $User, $message)
                    Write-Verbose "Updating Labels on Message '$message' for user '$User'"
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailMessageLabels'
function Update-GSGmailPopSettings {
    <#
    .SYNOPSIS
    Updates POP settings
 
    .DESCRIPTION
    Updates POP settings
 
    .PARAMETER User
    The user to update the POP settings for
 
    .PARAMETER AccessWindow
    The range of messages which are accessible via POP.
 
    Acceptable values are:
    * "accessWindowUnspecified": Unspecified range.
    * "allMail": Indicates that all unfetched messages are accessible via POP.
    * "disabled": Indicates that no messages are accessible via POP.
    * "fromNowOn": Indicates that unfetched messages received after some past point in time are accessible via POP.
 
    .PARAMETER Disposition
    The action that will be executed on a message after it has been fetched via POP.
 
    Acceptable values are:
    * "archive": Archive the message.
    * "dispositionUnspecified": Unspecified disposition.
    * "leaveInInbox": Leave the message in the INBOX.
    * "markRead": Leave the message in the INBOX and mark it as read.
    * "trash": Move the message to the TRASH.
 
    .EXAMPLE
    Update-GSGmailPopSettings -User me -AccessWindow allMail
 
    Sets the POP AccessWindow to 'allMail' for the AdminEmail user
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.PopSettings')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User,
        [parameter(Mandatory = $false)]
        [ValidateSet('accessWindowUnspecified','allMail','disabled','fromNowOn')]
        [string]
        $AccessWindow,
        [parameter(Mandatory = $false)]
        [ValidateSet('archive','dispositionUnspecified','leaveInInbox','markRead','trash')]
        [string]
        $Disposition
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            $body = New-Object 'Google.Apis.Gmail.v1.Data.PopSettings'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                $body.$prop = $PSBoundParameters[$prop]
            }
            $request = $service.Users.Settings.UpdatePop($body,$User)
            Write-Verbose "Updating POP settings for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailPopSettings'
function Update-GSGmailSendAsAlias {
    <#
    .SYNOPSIS
    Updates SendAs alias settings for a user.
 
    .DESCRIPTION
    Updates SendAs alias settings for a user.
 
    .PARAMETER User
    The user to update the SendAs settings for.
 
    .PARAMETER SendAsEmail
    The SendAs alias to be updated.
 
    .PARAMETER DisplayName
    A name that appears in the "From:" header for mail sent using this alias.
 
    For custom "from" addresses, when this is empty, Gmail will populate the "From:" header with the name that is used for the primary address associated with the account.
 
    If the admin has disabled the ability for users to update their name format, requests to update this field for the primary login will silently fail.
 
    .PARAMETER IsDefault
    Whether this address is selected as the default "From:" address in situations such as composing a new message or sending a vacation auto-reply.
 
    Every Gmail account has exactly one default send-as address, so the only legal value that clients may write to this field is true.
 
    Changing this from false to true for an address will result in this field becoming false for the other previous default address.
 
    .PARAMETER ReplyToAddress
    An optional email address that is included in a "Reply-To:" header for mail sent using this alias.
 
    If this is empty, Gmail will not generate a "Reply-To:" header.
 
    .PARAMETER Signature
    An optional HTML signature that is included in messages composed with this alias in the Gmail web UI.
 
    .PARAMETER SmtpMsa
    An optional SMTP service that will be used as an outbound relay for mail sent using this alias.
 
    If this is empty, outbound mail will be sent directly from Gmail's servers to the destination SMTP service. This setting only applies to custom "from" aliases.
 
    Use the helper function Add-GmailSmtpMsa to create the correct object for this parameter.
 
    .PARAMETER TreatAsAlias
    Whether Gmail should treat this address as an alias for the user's primary email address.
 
    This setting only applies to custom "from" aliases.
 
    .EXAMPLE
    $smtpMsa = Add-GSGmailSmtpMsa -Host 10.0.30.18 -Port 3770 -SecurityMode none -Username mailadmin -Password $(ConvertTo-SecureString $password -AsPlainText -Force)
    Update-GSGmailSendAsAlias -SendAsEmail joseph.wiggum@business.com -User joe@domain.com -Signature "<div>Thank you for your time,</br>Joseph Wiggum</div>" -SmtpMsa $smtpMsa
 
    Updates Joe's SendAs settings for his formal alias, including signature and SmtpMsa settings.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.SendAs')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $SendAsEmail,
        [parameter(Mandatory = $false)]
        [string]
        $DisplayName,
        [parameter(Mandatory = $false)]
        [switch]
        $IsDefault,
        [parameter(Mandatory = $false)]
        [string]
        $ReplyToAddress,
        [parameter(Mandatory = $false)]
        [string]
        $Signature,
        [parameter(Mandatory = $false)]
        [Google.Apis.Gmail.v1.Data.SmtpMsa]
        $SmtpMsa,
        [parameter(Mandatory = $false)]
        [switch]
        $TreatAsAlias
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        if (-not $PSBoundParameters.ContainsKey('SendAsEmail')) {
            $SendAsEmail = $User
        }
        elseif ($SendAsEmail -notlike "*@*.*") {
            $SendAsEmail = "$($SendAsEmail)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = @(
                'https://www.googleapis.com/auth/gmail.settings.sharing',
                'https://www.googleapis.com/auth/gmail.settings.basic'
            )
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Gmail.v1.Data.SendAs'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                $body.$prop = $PSBoundParameters[$prop]
            }
            $request = $service.Users.Settings.SendAs.Patch($body,$User,$SendAsEmail)
            Write-Verbose "Updating SendAs settings of alias '$SendAsEmail' for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailSendAsAlias'
function Update-GSGmailSignature {
    <#
    .SYNOPSIS
    Updates the Gmail signature for a user's SendAs alias.
 
    .DESCRIPTION
    Updates the Gmail signature for a user's SendAs alias.
 
    .PARAMETER User
    The user to update the SendAs signature for.
 
    .PARAMETER SendAsEmail
    The SendAs alias to be updated. Defaults to the User specified if this parameter is excluded.
 
    .PARAMETER Signature
    An HTML signature that is included in messages composed with this alias in the Gmail web UI.
 
    .PARAMETER SignatureFile
    A file containing the HTML signature that is included in messages composed with this alias in the Gmail web UI.
 
    The file will be read in with Get-Content $SignatureFile -Raw.
 
    .PARAMETER AsPlainText
    If $true, this wraps your Signature or SignatureFile contents in standard HTML that Google would normally add (div wrappers around each line with font-size:small on all and empty lines replaced with <br>).
 
    .EXAMPLE
    Update-GSGmailSignature -SendAsEmail joseph.wiggum@business.com -User joe@domain.com -Signature "<div>Thank you for your time,</br>Joseph Wiggum</div>"
 
    Updates Joe's SendAs signature for his formal alias.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.SendAs')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [string]
        $SendAsEmail,
        [parameter(Mandatory = $true,ValueFromPipeline = $true,ParameterSetName = "Signature")]
        [string]
        $Signature,
        [parameter(Mandatory = $true,ParameterSetName = "SignatureFile")]
        [ValidateScript({
            try {
                Test-Path $_ -ErrorAction Stop
                $true
            }
            catch {
                throw $_
            }
        })]
        [string]
        $SignatureFile,
        [parameter(Mandatory = $false)]
        [switch]
        $AsPlainText
    )
    Begin {
        $finalSignature = if ($AsPlainText) {
            $baseSignature = if ($PSCmdlet.ParameterSetName -eq 'SignatureFile') {
                Get-Content $SignatureFile
            }
            else {
                $Signature -split "[\r\n]" | Where-Object {$_}
            }
            $fmtSignature = @('<div dir="ltr"><br><div>')
            foreach ($line in $baseSignature) {
                if ([String]::IsNullOrEmpty(($line -replace "\s"))) {
                    $fmtSignature += '<div style="font-size:small"><br></div>'
                }
                else {
                    $fmtSignature += "<div style=`"font-size:small`">$line</div>"
                }
            }
            $fmtSignature += '</div></div>'
            $fmtSignature -join ""
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'SignatureFile') {
            Get-Content $SignatureFile -Raw
        }
        else {
            $Signature
        }
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        if (-not $PSBoundParameters.ContainsKey('SendAsEmail')) {
            $SendAsEmail = $User
        }
        elseif ($SendAsEmail -notlike "*@*.*") {
            $SendAsEmail = "$($SendAsEmail)@$($Script:PSGSuite.Domain)"
        }
        try {
            Write-Verbose "Setting the following signature on SendAs alias '$SendAsEmail' for user '$User':`n`n$finalSignature`n"
            Update-GSGmailSendAsAlias -SendAsEmail $SendAsEmail -User $User -Signature $finalSignature
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailSignature'
function Update-GSGmailVacationSettings {
    <#
    .SYNOPSIS
    Updates vacation responder settings for the specified account.
 
    .DESCRIPTION
    Updates vacation responder settings for the specified account.
 
    .PARAMETER User
    The user to update the VacationSettings settings for
 
    .PARAMETER EnableAutoReply
    Flag that controls whether Gmail automatically replies to messages.
 
    .PARAMETER EndTime
    An optional end time for sending auto-replies. When this is specified, Gmail will automatically reply only to messages that it receives before the end time. If both startTime and endTime are specified, startTime must precede endTime.
 
    .PARAMETER ResponseBodyHtml
    Response body in HTML format. Gmail will sanitize the HTML before storing it.
 
    .PARAMETER ResponseBodyPlainText
    Response body in plain text format.
 
    .PARAMETER ResponseSubject
    Optional text to prepend to the subject line in vacation responses. In order to enable auto-replies, either the response subject or the response body must be nonempty.
 
    .PARAMETER RestrictToContacts
    Flag that determines whether responses are sent to recipients who are not in the user's list of contacts.
 
    .PARAMETER RestrictToDomain
    Flag that determines whether responses are sent to recipients who are outside of the user's domain. This feature is only available for G Suite users.
 
    .PARAMETER StartTime
    An optional start time for sending auto-replies. When this is specified, Gmail will automatically reply only to messages that it receives after the start time. If both startTime and endTime are specified, startTime must precede endTime.
 
    .EXAMPLE
    Update-GSGmailVacationSettings -User me -ResponseBodyHtml "I'm on vacation and will reply when I'm back in the office. Thanks!" -RestrictToDomain -EndTime (Get-Date).AddDays(7) -StartTime (Get-Date) -EnableAutoReply
 
    Enables the vacation auto-reply for the AdminEmail user. Auto-replies will be sent to other users in the same domain only. The vacation response is enabled for 7 days from the time that the command is sent.
 
    .EXAMPLE
    Update-GSGmailVacationSettings -User me -EnableAutoReply:$false
 
    Disables the vacaction auto-response for the AdminEmail user immediately.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.VacationSettings')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [string]
        $User,
        [parameter(Mandatory = $false)]
        [switch]
        $EnableAutoReply,
        [parameter(Mandatory = $false)]
        [datetime]
        $EndTime,
        [parameter(Mandatory = $false)]
        [string]
        $ResponseBodyHtml,
        [parameter(Mandatory = $false)]
        [string]
        $ResponseBodyPlainText,
        [parameter(Mandatory = $false)]
        [string]
        $ResponseSubject,
        [parameter(Mandatory = $false)]
        [switch]
        $RestrictToContacts,
        [parameter(Mandatory = $false)]
        [switch]
        $RestrictToDomain,
        [parameter(Mandatory = $false)]
        [datetime]
        $StartTime
    )
    Begin {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/gmail.settings.basic'
            ServiceType = 'Google.Apis.Gmail.v1.GmailService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            $body = New-Object 'Google.Apis.Gmail.v1.Data.VacationSettings'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    StartTime {
                        $epochMs = (Convert-DateToEpoch $StartTime)
                        $body.$prop = [long]$epochMs
                    }
                    EndTime {
                        $epochMs = (Convert-DateToEpoch $EndTime)
                        $body.$prop = [long]$epochMs
                    }
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            $request = $service.Users.Settings.UpdateVacation($body,$User)
            Write-Verbose "Updating Vacation settings for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGmailVacationSettings'
function Add-GSGroupMember {
    <#
    .SYNOPSIS
    Adds a list of emails to a target group
 
    .DESCRIPTION
    Adds a list of emails to a target group. Designed for parity with Add-ADGroupMember
 
    .PARAMETER Identity
    The email or GroupID of the target group to add members to
 
    .PARAMETER Member
    The list of user and/or group emails that you would like to add to the target group
 
    .PARAMETER Role
    The role that you would like to add the members as
 
    Defaults to "MEMBER"
 
    .EXAMPLE
    Add-GSGroupMember "admins@domain.com" -Member "joe-admin@domain.com","sally.admin@domain.com"
 
    Adds 2 users to the group "admins@domain.com"
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Member')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String]
        $Identity,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,Position = 1)]
        [Alias("PrimaryEmail","UserKey","Mail","User","UserEmail","Members")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Member,
        [parameter(Mandatory = $false)]
        [ValidateSet("MEMBER","MANAGER","OWNER")]
        [String]
        $Role = "MEMBER"
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Resolve-Email ([ref]$Identity) -IsGroup
            $groupObj = Get-GSGroup -Group $Identity -Verbose:$false
            foreach ($U in $Member) {
                try {
                    Resolve-Email ([ref]$U)
                    Write-Verbose "Adding '$U' as a $Role of group '$Identity'"
                    $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Member'
                    $body.Email = $U
                    $body.Role = $Role
                    $request = $service.Members.Insert($body,$groupObj.Id)
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $Identity -PassThru
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSGroupMember'
function Add-GSPrincipalGroupMembership {
    <#
    .SYNOPSIS
    Adds the target email to a list of groups
 
    .DESCRIPTION
    Adds the target email to a list of groups. Designed for parity with Add-ADPrincipalGroupMembership
 
    .PARAMETER Identity
    The user or group email that you would like to add to the list of groups
 
    .PARAMETER MemberOf
    The list of groups to add the target email to
 
    .PARAMETER Role
    The role that you would like to add the members as
 
    Defaults to "MEMBER"
 
    .EXAMPLE
    Add-GSPrincipalGroupMembership "joe@domain.com" -MemberOf "admins@domain.com","users@domain.com"
 
    Adds the email "joe@domain.com" to the admins@ and users@ groups
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Member')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","User","Email","UserEmail")]
        [String]
        $Identity,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,Position = 1)]
        [Alias('GroupEmail','Group')]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $MemberOf,
        [parameter(Mandatory = $false)]
        [ValidateSet("MEMBER","MANAGER","OWNER")]
        [String]
        $Role = "MEMBER"
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Resolve-Email ([ref]$Identity)
            try {
                foreach ($U in $MemberOf) {
                    $groupObj = Get-GSGroup -Group $U -Verbose:$false
                    Write-Verbose "Adding '$Identity' as a $Role of group '$($groupObj.Email)'"
                    $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Member'
                    $body.Email = $Identity
                    $body.Role = $Role
                    $request = $service.Members.Insert($body,$groupObj.Id)
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $groupObj.Email -PassThru
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSPrincipalGroupMembership'
function Get-GSGroup {
    <#
    .SYNOPSIS
    Gets the specified group's information. Returns the full group list if -Group is excluded
 
    .DESCRIPTION
    Gets the specified group's information. Returns the full group list if -Group is excluded. Designed for parity with Get-ADGroup (although Google's API is unable to 'Filter' for groups)
 
    .PARAMETER Identity
    The group or list of groups you would like to retrieve info for. If excluded, returns the group list instead
 
    .PARAMETER Filter
    Query string search. Complete documentation is at https://developers.google.com/admin-sdk/directory/v1/guides/search-groups
 
    .PARAMETER Where_IsAMember
    Include a user email here to get the list of groups that user is a member of
 
    .PARAMETER Domain
    The domain name. Use this field to get fields from only one domain. To return groups for all domains you own, exclude this parameter
 
    .PARAMETER Fields
    The fields to return in the response
 
    .PARAMETER PageSize
    Page size of the result set
 
    Defaults to 200
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSGroup -Where_IsAMember "joe@domain.com"
 
    Gets the list of groups that joe@domain.com is a member of
 
    .EXAMPLE
    Get-GSGroup -Domain mysubdomain.org
 
    Gets the list of groups only for the 'mysubdomain.org' domain.
 
    .EXAMPLE
    Get-GSGroup -Filter "email:support*"
 
    Gets all the groups with emails beginning with 'support'
 
    .EXAMPLE
    Get-GSGroup -Filter "name -eq 'IT HelpDesk'"
 
    Gets the IT HelpDesk group by name using PowerShell syntax. PowerShell syntax is supported as a best effort, please refer to the Group Search documentation from Google for exact syntax.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Group')]
    [cmdletbinding(DefaultParameterSetName = "ListFilter")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Get")]
        [Alias("Email","Group","GroupEmail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Identity,
        [parameter(Mandatory = $false,ParameterSetName = "ListFilter")]
        [Alias('Query')]
        [string]
        $Filter,
        [parameter(Mandatory = $false,ParameterSetName = "ListWhereMember")]
        [Alias('UserKey')]
        [String]
        $Where_IsAMember,
        [parameter(Mandatory = $false,ParameterSetName = "ListFilter")]
        [string]
        $Domain,
        [parameter(Mandatory = $false,ParameterSetName = "Get")]
        [String[]]
        $Fields,
        [parameter(Mandatory = $false,ParameterSetName = "ListFilter")]
        [parameter(Mandatory = $false,ParameterSetName = "ListWhereMember")]
        [ValidateRange(1,200)]
        [Alias("MaxResults")]
        [Int]
        $PageSize = 200,
        [parameter(Mandatory = $false,ParameterSetName = "ListFilter")]
        [parameter(Mandatory = $false,ParameterSetName = "ListWhereMember")]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        switch -Regex ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($G in $Identity) {
                    try {
                        Resolve-Email ([ref]$G) -IsGroup
                        Write-Verbose "Getting group '$G'"
                        $request = $service.Groups.Get($G)
                        if ($Fields) {
                            $request.Fields = "$($Fields -join ",")"
                        }
                        $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $G -PassThru
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            'List.*' {
                $verbString = "Getting all G Suite Groups"
                try {
                    $request = $service.Groups.List()
                    if ($PSBoundParameters.Keys -contains 'Where_IsAMember') {
                        Resolve-Email ([ref]$Where_IsAMember)
                        $verbString += " where '$Where_IsAMember' is a member"
                        $request.UserKey = $Where_IsAMember
                    }
                    elseif ($PSBoundParameters.Keys -contains 'Filter') {
                        if ($Filter -eq '*') {
                            $Filter = ""
                        }
                        else {
                            $Filter = "$($Filter -join " ")"
                        }
                        $Filter = $Filter -replace " -eq ","=" -replace " -like ",":" -replace " -match ",":" -replace " -contains ",":" -creplace "'True'","True" -creplace "'False'","False"
                        if (-not [String]::IsNullOrEmpty($Filter.Trim())) {
                            $verbString += " matching query '$($Filter.Trim())'"
                            $request.Query = $Filter.Trim()
                        }
                    }
                    if ($PSBoundParameters.Keys -notcontains 'Where_IsAMember') {
                        if ($PSBoundParameters.Keys -contains 'Domain') {
                            $verbString += " for domain '$Domain'"
                            $request.Domain = $Domain
                        }
                        elseif ( -not [String]::IsNullOrEmpty($Script:PSGSuite.CustomerID)) {
                            $verbString += " for customer '$($Script:PSGSuite.CustomerID)'"
                            $request.Customer = $Script:PSGSuite.CustomerID
                        }
                        else {
                            $verbString += " for customer 'my_customer'"
                            $request.Customer = "my_customer"
                        }
                    }
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.MaxResults = $PageSize
                    Write-Verbose $verbString
                    [int]$i = 1
                    $overLimit = $false
                    do {
                        $result = $request.Execute()
                        if ($null -ne $result.GroupsValue) {
                            $result.GroupsValue | Add-Member -MemberType ScriptMethod -Name ToString -Value {$this.Email} -PassThru -Force
                        }
                        $request.PageToken = $result.NextPageToken
                        [int]$retrieved = ($i + $result.GroupsValue.Count) - 1
                        Write-Verbose "Retrieved $retrieved groups..."
                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                            Write-Verbose "Limit reached: $Limit"
                            $overLimit = $true
                        }
                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                            $newPS = $Limit - $retrieved
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                            $request.MaxResults = $newPS
                        }
                        [int]$i = $i + $result.GroupsValue.Count
                    }
                    until ($overLimit -or !$result.NextPageToken)
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGroup'
function Get-GSGroupAlias {
    <#
    .SYNOPSIS
    Gets the specified G SUite Group's aliases
 
    .DESCRIPTION
    Gets the specified G SUite Group's aliases
 
    .PARAMETER Identity
    The primary email or ID of the group who you are trying to get aliases for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    .EXAMPLE
    Get-GSGroupAlias -Identity hr
 
    Gets the list of aliases for the group hr@domain.com
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Alias')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String[]]
        $Identity
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($G in $Identity) {
            try {
                Resolve-Email ([ref]$G) -IsGroup
                Write-Verbose "Getting Alias list for Group '$G'"
                $request = $service.Groups.Aliases.List($G)
                $request.Execute() | Select-Object -ExpandProperty AliasesValue
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGroupAlias'
function Get-GSGroupMember {
    <#
    .SYNOPSIS
    Gets the group member list of a target group
 
    .DESCRIPTION
    Gets the group member list of a target group. Designed for parity with Get-ADGroupMember
 
    .PARAMETER Identity
    The email or unique ID of the target group
 
    .PARAMETER Member
    If specified, returns only the information for this member of the target group
 
    .PARAMETER Roles
    If specified, returns only the members of the specified role(s)
 
    .PARAMETER PageSize
    Page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSGroupMember "admins@domain.com" -Roles Owner,Manager
 
    Returns the list of owners and managers of the group "admins@domain.com"
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Member')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String[]]
        $Identity,
        [parameter(Mandatory = $false,Position = 1,ParameterSetName = "Get")]
        [Alias("PrimaryEmail","UserKey","Mail","User","UserEmail","Members")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Member,
        [parameter(Mandatory=$false,ParameterSetName = "List")]
        [ValidateSet("Owner","Manager","Member")]
        [String[]]
        $Roles,
        [parameter(Mandatory=$false,ParameterSetName = "List")]
        [ValidateRange(1,200)]
        [Int]
        $PageSize = 200,
        [parameter(Mandatory=$false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($I in $Identity) {
                    try {
                        Resolve-Email ([ref]$I) -IsGroup
                        foreach ($G in $Member) {
                            Resolve-Email ([ref]$G)
                            Write-Verbose "Getting member '$G' of group '$I'"
                            $request = $service.Members.Get($I,$G)
                            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $I -PassThru
                        }
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            List {
                foreach ($Id in $Identity) {
                    try {
                        Resolve-Email ([ref]$Id) -IsGroup
                        $request = $service.Members.List($Id)
                        if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                            $PageSize = $Limit
                        }
                        $request.MaxResults = $PageSize
                        if ($Roles) {
                            Write-Verbose "Getting all members of group '$Id' in the following role(s): $($Roles -join ',')"
                            $request.Roles = "$($Roles -join ',')"
                        }
                        else {
                            Write-Verbose "Getting all members of group '$Id'"
                        }
                        [int]$i = 1
                        $overLimit = $false
                        do {
                            $result = $request.Execute()
                            if ($null -ne $result.MembersValue) {
                                $result.MembersValue | Add-Member -MemberType NoteProperty -Name 'Group' -Value $Id -PassThru  | Add-Member -MemberType ScriptMethod -Name ToString -Value {$this.Email} -PassThru -Force
                            }
                            $request.PageToken = $result.NextPageToken
                            [int]$retrieved = ($i + $result.MembersValue.Count) - 1
                            Write-Verbose "Retrieved $retrieved members..."
                            if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                                Write-Verbose "Limit reached: $Limit"
                                $overLimit = $true
                            }
                            elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                                $newPS = $Limit - $retrieved
                                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                                $request.MaxResults = $newPS
                            }
                            [int]$i = $i + $result.MembersValue.Count
                        }
                        until ($overLimit -or !$result.NextPageToken)
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGroupMember'
function Get-GSGroupSettings {
    <#
    .SYNOPSIS
    Gets a group's settings
 
    .DESCRIPTION
    Gets a group's settings
 
    .PARAMETER Identity
    The email or unique ID of the group.
 
    If only the email name-part is passed, the full email will be contstructed using the Domain from the active config
 
    .EXAMPLE
    Get-GSGroupSettings admins
 
    Gets the group settings for admins@domain.com
    #>

    [OutputType('Google.Apis.Groupssettings.v1.Data.Groups')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String[]]
        $Identity
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/apps.groups.settings'
            ServiceType = 'Google.Apis.Groupssettings.v1.GroupssettingsService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($G in $Identity) {
                Resolve-Email ([ref]$G) -IsGroup
                if ($G -notmatch '^[\w.%+-]+@[a-zA-Z\d.-]+\.[a-zA-Z]{2,}$') {
                    Write-Verbose "Getting Group Email for ID '$G' as the Group Settings API only accepts Group Email addresses."
                    $G = Get-GSGroup -Identity $G -Verbose:$false | Select-Object -ExpandProperty Email
                }
                Write-Verbose "Getting settings for group '$G'"
                $request = $service.Groups.Get($G)
                $request.Alt = "Json"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $G -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSGroupSettings'
function New-GSGroup {
    <#
    .SYNOPSIS
    Creates a new Google Group
 
    .DESCRIPTION
    Creates a new Google Group
 
    .PARAMETER Email
    The desired email of the new group. If the group already exists, a GoogleApiException will be thrown. You can exclude the '@domain.com' to insert the Domain in the config
 
    .PARAMETER Name
    The name of the new group
 
    .PARAMETER Description
    The description of the new group
 
    .EXAMPLE
    New-GSGroup -Email appdev -Name "Application Developers" -Description "App Dev team members"
 
    Creates a new group named "Application Developers" with the email "appdev@domain.com" and description "App Dev team members"
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Group')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true)]
        [String]
        $Email,
        [parameter(Mandatory = $true)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [String]
        $Description
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Resolve-Email ([ref]$Email)
            Write-Verbose "Creating group '$Email'"
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Group'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    Email {
                        $body.$prop = $Email
                    }
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            $request = $service.Groups.Insert($body)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSGroup'
function New-GSGroupAlias {
    <#
    .SYNOPSIS
    Creates a new alias for a G Suite group
 
    .DESCRIPTION
    Creates a new alias for a G Suite group
 
    .PARAMETER Identity
    The group to create the alias for
 
    .PARAMETER Alias
    The alias or list of aliases to create for the group
 
    .EXAMPLE
    New-GSGroupAlias -Identity humanresources@domain.com -Alias 'hr@domain.com','hrhelp@domain.com'
 
    Creates 2 new aliases for group Human Resources as 'hr@domain.com' and 'hrhelp@domain.com'
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Alias')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String]
        $Identity,
        [parameter(Mandatory = $true,Position = 1)]
        [String[]]
        $Alias
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        Resolve-Email ([ref]$Identity) -IsGroup
        foreach ($A in $Alias) {
            try {
                Resolve-Email ([ref]$A)
                Write-Verbose "Creating alias '$A' for Group '$Identity'"
                $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Alias'
                $body.AliasValue = $A
                $request = $service.Groups.Aliases.Insert($body,$Identity)
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSGroupAlias'
function Remove-GSGroup {
    <#
    .SYNOPSIS
    Removes a group
 
    .DESCRIPTION
    Removes a group
 
    .PARAMETER Identity
    The email or unique Id of the group to removed
 
    .EXAMPLE
    Remove-GSGroup 'test_group' -Confirm:$false
 
    Removes the group 'test_group@domain.com' without asking for confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String[]]
        $Identity
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($G in $Identity) {
                Resolve-Email ([ref]$G) -IsGroup
                if ($PSCmdlet.ShouldProcess("Removing group '$G'")) {
                    Write-Verbose "Removing group '$G'"
                    $request = $service.Groups.Delete($G)
                    $request.Execute()
                    Write-Verbose "Group '$G' has been successfully removed"
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGroup'
function Remove-GSGroupAlias {
    <#
    .SYNOPSIS
    Removes an alias from a G Suite group
 
    .DESCRIPTION
    Removes an alias from a G Suite group
 
    .PARAMETER Identity
    The group to remove the alias from
 
    .PARAMETER Alias
    The alias or list of aliases to remove from the group
 
    .EXAMPLE
    Remove-GSGroupAlias -Identity humanresources@domain.com -Alias 'hr@domain.com','hrhelp@domain.com'
 
    Removes 2 aliases for group Human Resources: 'hr@domain.com' and 'hrhelp@domain.com'
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String]
        $Identity,
        [parameter(Mandatory = $true,Position = 1)]
        [String[]]
        $Alias
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        Resolve-Email ([ref]$Identity) -IsGroup
        foreach ($A in $Alias) {
            try {
                Resolve-Email ([ref]$A)
                if ($PSCmdlet.ShouldProcess("Removing alias '$A' from Group '$Identity'")) {
                    Write-Verbose "Removing alias '$A' from Group '$Identity'"
                    $request = $service.Groups.Aliases.Delete($Identity,$A)
                    $request.Execute()
                    Write-Verbose "Alias '$A' has been successfully deleted from Group '$Identity'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGroupAlias'
function Remove-GSGroupMember {
    <#
    .SYNOPSIS
    Removes members from a group
 
    .DESCRIPTION
    Removes members from a group
 
    .PARAMETER Identity
    The email or unique Id of the group to remove members from
 
    .PARAMETER Member
    The member or array of members to remove from the target group
 
    .EXAMPLE
    Remove-GSGroupMember -Identity admins -Member joe.smith,mark.taylor -Confirm:$false
 
    Removes members Joe Smith and Mark Taylor from the group admins@domain.com and skips asking for confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String]
        $Identity,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,Position = 1)]
        [Alias("PrimaryEmail","UserKey","Mail","User","UserEmail","Members")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Member
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        Resolve-Email ([ref]$Identity) -IsGroup
        foreach ($G in $Member) {
            try {
                Resolve-Email ([ref]$G)
                if ($PSCmdlet.ShouldProcess("Removing member '$G' from group '$Identity'")) {
                    Write-Verbose "Removing member '$G' from group '$Identity'"
                    $request = $service.Members.Delete($Identity,$G)
                    $request.Execute()
                    Write-Verbose "Member '$G' has been successfully removed"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSGroupMember'
function Remove-GSPrincipalGroupMembership {
    <#
    .SYNOPSIS
    Removes the target member from a group or list of groups
 
    .DESCRIPTION
    Removes the target member from a group or list of groups
 
    .PARAMETER Identity
    The email or unique Id of the member you would like to remove from the group(s)
 
    .PARAMETER MemberOf
    The group(s) to remove the member from
 
    .EXAMPLE
    Remove-GSPrincipalGroupMembership -Identity 'joe.smith' -MemberOf admins,test_pool
 
    Removes Joe Smith from the groups admins@domain.com and test_pool@domain.com
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","User","UserEmail")]
        [String]
        $Identity,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,Position = 1)]
        [Alias('GroupEmail','Group','Email')]
        [String[]]
        $MemberOf
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        Resolve-Email ([ref]$Identity)
        foreach ($G in $MemberOf) {
            try {
                Resolve-Email ([ref]$G) -IsGroup
                if ($PSCmdlet.ShouldProcess("Removing member '$Identity' from group '$G'")) {
                    Write-Verbose "Removing member '$Identity' from group '$G'"
                    $request = $service.Members.Delete($G,$Identity)
                    $request.Execute()
                    Write-Verbose "Member '$Identity' has been successfully removed from group '$G'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSPrincipalGroupMembership'
function Set-GSGroupSettings {
    <#
    .SYNOPSIS
    Hard-sets the settings for a group
 
    .DESCRIPTION
    Hard-sets the settings for a group
 
    .PARAMETER Identity
    The primary email or unique Id of the group to set
 
    .PARAMETER Name
    The new name of the group
 
    .PARAMETER Description
    The new description of the group
 
    .PARAMETER ArchiveOnly
    If the group is archive only
 
    .PARAMETER AllowExternalMembers
    Are external members allowed to join the group
 
    .PARAMETER AllowGoogleCommunication
    Is google allowed to contact admins
 
    .PARAMETER AllowWebPosting
    If posting from web is allowed
 
    .PARAMETER CustomFooterText
    Custom footer text
 
    .PARAMETER CustomReplyToAddress
    Default email to which reply to any message should go
 
    .PARAMETER DefaultMessageDenyNotificationText
    Default message deny notification message
 
    .PARAMETER Email
    Email id of the group
 
    .PARAMETER IncludeCustomFooter
    Whether to include custom footer
 
    .PARAMETER IncludeInGlobalAddressList
    If this groups should be included in global address list or not
 
    .PARAMETER IsArchived
    If the contents of the group are archived
 
    .PARAMETER MaxMessageBytes
    Maximum message size allowed
 
    .PARAMETER MembersCanPostAsTheGroup
    Can members post using the group email address
 
    .PARAMETER MessageDisplayFont
    Default message display font
 
    Available values are:
    * "DEFAULT_FONT"
    * "FIXED_WIDTH_FONT"
 
    .PARAMETER MessageModerationLevel
    Moderation level for messages
 
    Available values are:
    * "MODERATE_ALL_MESSAGES"
    * "MODERATE_NON_MEMBERS"
    * "MODERATE_NEW_MEMBERS"
    * "MODERATE_NONE"
 
    .PARAMETER ReplyTo
    Who should the default reply to a message go to
 
    Available values are:
    * "REPLY_TO_CUSTOM"
    * "REPLY_TO_SENDER"
    * "REPLY_TO_LIST"
    * "REPLY_TO_OWNER"
    * "REPLY_TO_IGNORE"
    * "REPLY_TO_MANAGERS"
 
    .PARAMETER SendMessageDenyNotification
    Should the member be notified if his message is denied by owner
 
    .PARAMETER ShowInGroupDirectory
    Is the group listed in groups directory
 
    .PARAMETER SpamModerationLevel
    Moderation level for messages detected as spam
 
    Available values are:
    * "ALLOW"
    * "MODERATE"
    * "SILENTLY_MODERATE"
    * "REJECT"
 
    .PARAMETER WhoCanAdd
    Permissions to add members
 
    Available values are:
    * "ALL_MANAGERS_CAN_ADD"
    * "ALL_MEMBERS_CAN_ADD"
    * "NONE_CAN_ADD"
 
    .PARAMETER WhoCanContactOwner
    Permission to contact owner of the group via web UI
 
    Available values are:
    * "ANYONE_CAN_CONTACT"
    * "ALL_IN_DOMAIN_CAN_CONTACT"
    * "ALL_MEMBERS_CAN_CONTACT"
    * "ALL_MANAGERS_CAN_CONTACT"
 
    .PARAMETER WhoCanInvite
    Permissions to invite members.
    Available values are:
    * "ALL_MEMBERS_CAN_INVITE"
    * "ALL_MANAGERS_CAN_INVITE"
    * "NONE_CAN_INVITE"
 
    .PARAMETER WhoCanJoin
    Permissions to join the group.
    Available values are:
    * "ANYONE_CAN_JOIN"
    * "ALL_IN_DOMAIN_CAN_JOIN"
    * "INVITED_CAN_JOIN"
    * "CAN_REQUEST_TO_JOIN"
 
    .PARAMETER WhoCanLeaveGroup
    Permission to leave the group.
 
    Available values are:
    * "ALL_MANAGERS_CAN_LEAVE"
    * "ALL_MEMBERS_CAN_LEAVE"
    * "NONE_CAN_LEAVE"
 
    .PARAMETER WhoCanPostMessage
    Permissions to post messages to the group.
 
    Available values are:
    * "NONE_CAN_POST"
    * "ALL_MANAGERS_CAN_POST"
    * "ALL_MEMBERS_CAN_POST"
    * "ALL_OWNERS_CAN_POST"
    * "ALL_IN_DOMAIN_CAN_POST"
    * "ANYONE_CAN_POST"
 
    .PARAMETER WhoCanViewGroup
    Permissions to view group.
 
    Available values are:
    * "ANYONE_CAN_VIEW"
    * "ALL_IN_DOMAIN_CAN_VIEW"
    * "ALL_MEMBERS_CAN_VIEW"
    * "ALL_MANAGERS_CAN_VIEW"
 
    .PARAMETER WhoCanViewMembership
    Permissions to view membership.
 
    Available values are:
    * "ALL_IN_DOMAIN_CAN_VIEW"
    * "ALL_MEMBERS_CAN_VIEW"
    * "ALL_MANAGERS_CAN_VIEW"
 
    .EXAMPLE
    Set-GSGroupSettings admins,hr-notifications -AllowExternalMembers:$false -WhoCanPostMessage ALL_OWNERS_CAN_POST
 
    Sets the group settings for both admins@domain.com and hr-notifications@domain.com to deny external members and limit posting to only group owners
    #>

    [OutputType('Google.Apis.Groupssettings.v1.Data.Groups')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group')]
        [String[]]
        $Identity,
        [parameter(Mandatory = $false)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [Switch]
        $ArchiveOnly,
        [parameter(Mandatory = $false)]
        [Switch]
        $AllowExternalMembers,
        [parameter(Mandatory = $false)]
        [Switch]
        $AllowGoogleCommunication,
        [parameter(Mandatory = $false)]
        [Switch]
        $AllowWebPosting,
        [parameter(Mandatory = $false)]
        [ValidateScript( {$_.length -le 1000})]
        [String]
        $CustomFooterText,
        [parameter(Mandatory = $false)]
        [String]
        $CustomReplyToAddress,
        [parameter(Mandatory = $false)]
        [Alias('MessageDenyNotificationText')]
        [ValidateScript( {$_.length -le 10000})]
        [String]
        $DefaultMessageDenyNotificationText,
        [parameter(Mandatory = $false)]
        [String]
        $Email,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeCustomFooter,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeInGlobalAddressList,
        [parameter(Mandatory = $false)]
        [Switch]
        $IsArchived,
        [parameter(Mandatory = $false)]
        [int]
        $MaxMessageBytes,
        [parameter(Mandatory = $false)]
        [Switch]
        $MembersCanPostAsTheGroup,
        [parameter(Mandatory = $false)]
        [ValidateSet("DEFAULT_FONT","FIXED_WIDTH_FONT")]
        [String]
        $MessageDisplayFont,
        [parameter(Mandatory = $false)]
        [ValidateSet("MODERATE_ALL_MESSAGES","MODERATE_NEW_MEMBERS","MODERATE_NONE","MODERATE_NON_MEMBERS")]
        [String]
        $MessageModerationLevel,
        [parameter(Mandatory = $false)]
        [ValidateSet("REPLY_TO_CUSTOM","REPLY_TO_IGNORE","REPLY_TO_LIST","REPLY_TO_MANAGERS","REPLY_TO_OWNER","REPLY_TO_SENDER")]
        [String]
        $ReplyTo,
        [parameter(Mandatory = $false)]
        [Switch]
        $SendMessageDenyNotification,
        [parameter(Mandatory = $false)]
        [Switch]
        $ShowInGroupDirectory,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALLOW","MODERATE","SILENTLY_MODERATE","REJECT")]
        [String]
        $SpamModerationLevel,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_MEMBERS_CAN_ADD","ALL_MANAGERS_CAN_ADD","NONE_CAN_ADD")]
        [String]
        $WhoCanAdd,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_CONTACT","ALL_MANAGERS_CAN_CONTACT","ALL_MEMBERS_CAN_CONTACT","ANYONE_CAN_CONTACT")]
        [String]
        $WhoCanContactOwner,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_MANAGERS_CAN_INVITE","ALL_MEMBERS_CAN_INVITE","NONE_CAN_INVITE")]
        [String]
        $WhoCanInvite,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_JOIN","ANYONE_CAN_JOIN","CAN_REQUEST_TO_JOIN","INVITED_CAN_JOIN")]
        [String]
        $WhoCanJoin,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_MANAGERS_CAN_LEAVE","ALL_MEMBERS_CAN_LEAVE","NONE_CAN_LEAVE")]
        [String]
        $WhoCanLeaveGroup,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_POST","ALL_MANAGERS_CAN_POST","ALL_MEMBERS_CAN_POST","ANYONE_CAN_POST","NONE_CAN_POST")]
        [String]
        $WhoCanPostMessage,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_VIEW","ALL_MANAGERS_CAN_VIEW","ALL_MEMBERS_CAN_VIEW","ANYONE_CAN_VIEW")]
        [String]
        $WhoCanViewGroup,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_VIEW","ALL_MANAGERS_CAN_VIEW","ALL_MEMBERS_CAN_VIEW")]
        [String]
        $WhoCanViewMembership
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/apps.groups.settings'
            ServiceType = 'Google.Apis.Groupssettings.v1.GroupssettingsService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($G in $Identity) {
                Resolve-Email ([ref]$G) -IsGroup
                Write-Verbose "Updating settings for group '$G'"
                $body = New-Object 'Google.Apis.Groupssettings.v1.Data.Groups'
                foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                    switch ($prop) {
                        MaxMessageBytes {
                            $body.$prop = $PSBoundParameters[$prop]
                        }
                        Default {
                            $body.$prop = if ($PSBoundParameters[$prop].ToString() -in @("True","False")) {
                                $($PSBoundParameters[$prop]).ToString().ToLower()
                            }
                            else {
                                $PSBoundParameters[$prop]
                            }
                        }
                    }
                }
                $request = $service.Groups.Update($body,$G)
                $request.Alt = "Json"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $G -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Set-GSGroupSettings'
function Test-GSGroupMembership {
    <#
    .SYNOPSIS
    Checks if a Group has a specific member
 
    .DESCRIPTION
    Checks if a Group has a specific member
 
    .PARAMETER Identity
    The email of the group
 
    If only the email name-part is passed, the full email will be contstructed using the Domain from the active config
 
    .PARAMETER Member
    The user to confirm as a member of the Group
 
    .EXAMPLE
    Test-GSGroupMembership -Identity admins@domain.com -Member john@domain.com
 
    Tests if john@domain.com is a member of admins@domain.com
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.MembersHasMember')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group','Email')]
        [String]
        $Identity,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,Position = 1)]
        [Alias("PrimaryEmail","UserKey","Mail","User","UserEmail","Members")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Member
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        Resolve-Email ([ref]$Identity) -IsGroup
        foreach ($mem in $Member) {
            try {
                Resolve-Email ([ref]$mem)
                Write-Verbose "Checking if group '$Identity' has member '$mem'"
                $request = $service.Members.HasMember($Identity,$mem)
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $Identity -Force -PassThru | Add-Member -MemberType NoteProperty -Name 'Member' -Value $mem -Force -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Test-GSGroupMembership'
function Update-GSGroupMember {
    <#
    .SYNOPSIS
    Updates a group member's role and/or delivery preference
 
    .DESCRIPTION
    Updates a group member's role and/or delivery preference
 
    .PARAMETER Identity
    The email or unique ID of the group to update members of
 
    .PARAMETER Member
    The member email or list of member emails that you would like to update
 
    .PARAMETER Role
    The role that you would like to update the members to
 
    Acceptable values are:
    * MEMBER
    * MANAGER
    * OWNER
 
    .PARAMETER DeliverySettings
    Defines mail delivery preferences of member
 
    Acceptable values are:
    * "ALL_MAIL": All messages, delivered as soon as they arrive.
    * "DAILY": No more than one message a day.
    * "DIGEST": Up to 25 messages bundled into a single message.
    * "DISABLED": Remove subscription.
    * "NONE": No messages.
 
    .EXAMPLE
    Get-GSGroupMember myGroup | Update-GSGroupMember -DeliverySettings ALL_MAIL
 
    Updates the delivery preference for all members of group 'myGroup@domain.com' to 'ALL_MAIL'
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Member')]
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail', 'Group')]
        [String]
        $Identity,
        [parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail", "UserKey", "Mail", "User", "UserEmail", "Email", "Members")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Member,
        [parameter(Mandatory = $false)]
        [ValidateSet("MEMBER", "MANAGER", "OWNER")]
        [String]
        $Role,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_MAIL", "DAILY", "DIGEST", "DISABLED", "NONE")]
        [String]
        $DeliverySettings
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.group'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        Resolve-Email ([ref]$Identity) -IsGroup
        foreach ($U in $Member) {
            try {
                Resolve-Email ([ref]$U)
                Write-Verbose "Updating member '$U' of group '$Identity'"
                $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Member'
                if ($PSBoundParameters.Keys -contains 'DeliverySettings') {
                    $body.DeliverySettings = $PSBoundParameters['DeliverySettings']
                }
                if ($PSBoundParameters.Keys -contains 'Role') {
                    $body.Role = $PSBoundParameters['Role']
                }
                $request = $service.Members.Patch($body, $Identity, $U)
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $Identity -PassThru
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGroupMember'
function Update-GSGroupSettings {
    <#
    .SYNOPSIS
    Updates the settings for a group while retaining excluded, already existing settings
 
    .DESCRIPTION
    Updates the settings for a group while retaining excluded, already existing settings
 
    .PARAMETER Identity
    The primary email or unique Id of the group to update
 
    .PARAMETER Name
    The new name of the group
 
    .PARAMETER Description
    The new description of the group
 
    .PARAMETER ArchiveOnly
    If the group is archive only
 
    .PARAMETER AllowExternalMembers
    Are external members allowed to join the group
 
    .PARAMETER AllowGoogleCommunication
    Is google allowed to contact admins
 
    .PARAMETER AllowWebPosting
    If posting from web is allowed
 
    .PARAMETER CustomFooterText
    Custom footer text
 
    .PARAMETER CustomReplyToAddress
    Default email to which reply to any message should go
 
    .PARAMETER DefaultMessageDenyNotificationText
    Default message deny notification message
 
    .PARAMETER Email
    Email id of the group
 
    .PARAMETER EnableCollaborativeInbox
    Specifies whether the collaborative inbox functionality will be turned on or off for the group.
 
    .PARAMETER IncludeCustomFooter
    Whether to include custom footer
 
    .PARAMETER IncludeInGlobalAddressList
    If this groups should be included in global address list or not
 
    .PARAMETER IsArchived
    If the contents of the group are archived
 
    .PARAMETER MaxMessageBytes
    Maximum message size allowed
 
    .PARAMETER MembersCanPostAsTheGroup
    Can members post using the group email address
 
    .PARAMETER MessageDisplayFont
    Default message display font
 
    Available values are:
    * "DEFAULT_FONT"
    * "FIXED_WIDTH_FONT"
 
    .PARAMETER MessageModerationLevel
    Moderation level for messages
 
    Available values are:
    * "MODERATE_ALL_MESSAGES"
    * "MODERATE_NON_MEMBERS"
    * "MODERATE_NEW_MEMBERS"
    * "MODERATE_NONE"
 
    .PARAMETER ReplyTo
    Who should the default reply to a message go to
 
    Available values are:
    * "REPLY_TO_CUSTOM"
    * "REPLY_TO_SENDER"
    * "REPLY_TO_LIST"
    * "REPLY_TO_OWNER"
    * "REPLY_TO_IGNORE"
    * "REPLY_TO_MANAGERS"
 
    .PARAMETER SendMessageDenyNotification
    Should the member be notified if his message is denied by owner
 
    .PARAMETER ShowInGroupDirectory
    Is the group listed in groups directory
 
    .PARAMETER SpamModerationLevel
    Moderation level for messages detected as spam
 
    Available values are:
    * "ALLOW"
    * "MODERATE"
    * "SILENTLY_MODERATE"
    * "REJECT"
 
    .PARAMETER WhoCanAdd
    Permissions to add members
 
    Available values are:
    * "ALL_MANAGERS_CAN_ADD"
    * "ALL_MEMBERS_CAN_ADD"
    * "NONE_CAN_ADD"
 
    .PARAMETER WhoCanContactOwner
    Permission to contact owner of the group via web UI
 
    Available values are:
    * "ANYONE_CAN_CONTACT"
    * "ALL_IN_DOMAIN_CAN_CONTACT"
    * "ALL_MEMBERS_CAN_CONTACT"
    * "ALL_MANAGERS_CAN_CONTACT"
 
    .PARAMETER WhoCanDiscoverGroup
    Specifies the set of users for whom this group is discoverable.
    Available values are:
    * "ANYONE_CAN_DISCOVER"
    * "ALL_IN_DOMAIN_CAN_DISCOVER"
    * "ALL_MEMBERS_CAN_DISCOVER"
 
    .PARAMETER WhoCanInvite
    Permissions to invite members.
    Available values are:
    * "ALL_MEMBERS_CAN_INVITE"
    * "ALL_MANAGERS_CAN_INVITE"
    * "NONE_CAN_INVITE"
 
    .PARAMETER WhoCanJoin
    Permissions to join the group.
    Available values are:
    * "ANYONE_CAN_JOIN"
    * "ALL_IN_DOMAIN_CAN_JOIN"
    * "INVITED_CAN_JOIN"
    * "CAN_REQUEST_TO_JOIN"
 
    .PARAMETER WhoCanLeaveGroup
    Permission to leave the group.
 
    Available values are:
    * "ALL_MANAGERS_CAN_LEAVE"
    * "ALL_MEMBERS_CAN_LEAVE"
    * "NONE_CAN_LEAVE"
 
    .PARAMETER WhoCanPostMessage
    Permissions to post messages to the group.
 
    Available values are:
    * "NONE_CAN_POST"
    * "ALL_MANAGERS_CAN_POST"
    * "ALL_MEMBERS_CAN_POST"
    * "ALL_OWNERS_CAN_POST"
    * "ALL_IN_DOMAIN_CAN_POST"
    * "ANYONE_CAN_POST"
 
    .PARAMETER WhoCanViewGroup
    Permissions to view group.
 
    Available values are:
    * "ANYONE_CAN_VIEW"
    * "ALL_IN_DOMAIN_CAN_VIEW"
    * "ALL_MEMBERS_CAN_VIEW"
    * "ALL_MANAGERS_CAN_VIEW"
 
    .PARAMETER WhoCanViewMembership
    Permissions to view membership.
 
    Available values are:
    * "ALL_IN_DOMAIN_CAN_VIEW"
    * "ALL_MEMBERS_CAN_VIEW"
    * "ALL_MANAGERS_CAN_VIEW"
 
    .EXAMPLE
    Updates-GSGroupSettings admins,hr-notifications -AllowExternalMembers:$false -WhoCanPostMessage ALL_OWNERS_CAN_POST
 
    Updates the group settings for both admins@domain.com and hr-notifications@domain.com to deny external members and limit posting to only group owners
    #>

    [OutputType('Google.Apis.Groupssettings.v1.Data.Groups')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupEmail','Group')]
        [String[]]
        $Identity,
        [parameter(Mandatory = $false)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [Switch]
        $ArchiveOnly,
        [parameter(Mandatory = $false)]
        [Switch]
        $AllowExternalMembers,
        [parameter(Mandatory = $false)]
        [Switch]
        $AllowGoogleCommunication,
        [parameter(Mandatory = $false)]
        [Switch]
        $AllowWebPosting,
        [parameter(Mandatory = $false)]
        [ValidateScript( {$_.length -le 1000})]
        [String]
        $CustomFooterText,
        [parameter(Mandatory = $false)]
        [String]
        $CustomReplyToAddress,
        [parameter(Mandatory = $false)]
        [Alias('MessageDenyNotificationText')]
        [ValidateScript( {$_.length -le 10000})]
        [String]
        $DefaultMessageDenyNotificationText,
        [parameter(Mandatory = $false)]
        [String]
        $Email,
        [parameter(Mandatory = $false)]
        [Switch]
        $EnableCollaborativeInbox,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeCustomFooter,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeInGlobalAddressList,
        [parameter(Mandatory = $false)]
        [Switch]
        $IsArchived,
        [parameter(Mandatory = $false)]
        [int]
        $MaxMessageBytes,
        [parameter(Mandatory = $false)]
        [Switch]
        $MembersCanPostAsTheGroup,
        [parameter(Mandatory = $false)]
        [ValidateSet("DEFAULT_FONT","FIXED_WIDTH_FONT")]
        [String]
        $MessageDisplayFont,
        [parameter(Mandatory = $false)]
        [ValidateSet("MODERATE_ALL_MESSAGES","MODERATE_NEW_MEMBERS","MODERATE_NONE","MODERATE_NON_MEMBERS")]
        [String]
        $MessageModerationLevel,
        [parameter(Mandatory = $false)]
        [ValidateSet("REPLY_TO_CUSTOM","REPLY_TO_IGNORE","REPLY_TO_LIST","REPLY_TO_MANAGERS","REPLY_TO_OWNER","REPLY_TO_SENDER")]
        [String]
        $ReplyTo,
        [parameter(Mandatory = $false)]
        [Switch]
        $SendMessageDenyNotification,
        [parameter(Mandatory = $false)]
        [Switch]
        $ShowInGroupDirectory,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALLOW","MODERATE","SILENTLY_MODERATE","REJECT")]
        [String]
        $SpamModerationLevel,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_MEMBERS_CAN_ADD","ALL_MANAGERS_CAN_ADD","NONE_CAN_ADD")]
        [String]
        $WhoCanAdd,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_CONTACT","ALL_MANAGERS_CAN_CONTACT","ALL_MEMBERS_CAN_CONTACT","ANYONE_CAN_CONTACT")]
        [String]
        $WhoCanContactOwner,
        [parameter(Mandatory = $false)]
        [ValidateSet("ANYONE_CAN_DISCOVER","ALL_IN_DOMAIN_CAN_DISCOVER","ALL_MEMBERS_CAN_DISCOVER")]
        [String]
        $WhoCanDiscoverGroup,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_MANAGERS_CAN_INVITE","ALL_MEMBERS_CAN_INVITE","NONE_CAN_INVITE")]
        [String]
        $WhoCanInvite,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_JOIN","ANYONE_CAN_JOIN","CAN_REQUEST_TO_JOIN","INVITED_CAN_JOIN")]
        [String]
        $WhoCanJoin,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_MANAGERS_CAN_LEAVE","ALL_MEMBERS_CAN_LEAVE","NONE_CAN_LEAVE")]
        [String]
        $WhoCanLeaveGroup,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_POST","ALL_MANAGERS_CAN_POST","ALL_MEMBERS_CAN_POST","ANYONE_CAN_POST","NONE_CAN_POST")]
        [String]
        $WhoCanPostMessage,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_VIEW","ALL_MANAGERS_CAN_VIEW","ALL_MEMBERS_CAN_VIEW","ANYONE_CAN_VIEW")]
        [String]
        $WhoCanViewGroup,
        [parameter(Mandatory = $false)]
        [ValidateSet("ALL_IN_DOMAIN_CAN_VIEW","ALL_MANAGERS_CAN_VIEW","ALL_MEMBERS_CAN_VIEW")]
        [String]
        $WhoCanViewMembership
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/apps.groups.settings'
            ServiceType = 'Google.Apis.Groupssettings.v1.GroupssettingsService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($G in $Identity) {
                Resolve-Email ([ref]$G) -IsGroup
                if ($G -notmatch '^[\w.%+-]+@[a-zA-Z\d.-]+\.[a-zA-Z]{2,}$') {
                    Write-Verbose "Getting Group Email for ID '$G' as the Group Settings API only accepts Group Email addresses."
                    $G = Get-GSGroup -Identity $G -Verbose:$false | Select-Object -ExpandProperty Email
                }
                Write-Verbose "Updating settings for group '$G'"
                $body = New-Object 'Google.Apis.Groupssettings.v1.Data.Groups'
                foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                    switch ($prop) {
                        MaxMessageBytes {
                            $body.$prop = $PSBoundParameters[$prop]
                        }
                        Default {
                            $body.$prop = if ($PSBoundParameters[$prop].ToString() -in @("True","False")) {
                                $($PSBoundParameters[$prop]).ToString().ToLower()
                            }
                            else {
                                $PSBoundParameters[$prop]
                            }
                        }
                    }
                }
                $request = $service.Groups.Patch($body,$G)
                $request.Alt = "Json"
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Group' -Value $G -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSGroupSettings'
function Add-GSCalendarEventReminder {
    <#
    .SYNOPSIS
    Builds an EventReminder object to use when creating or updating a CalendarSubscription or CalendarEvent
 
    .DESCRIPTION
    Builds an EventReminder object to use when creating or updating a CalendarSubscription or CalendarEvent
 
    .PARAMETER Method
    The method used by this reminder. Defaults to email.
 
    Possible values are:
    * "email" - Reminders are sent via email.
    * "sms" - Reminders are sent via SMS. These are only available for G Suite customers. Requests to set SMS reminders for other account types are ignored.
    * "popup" - Reminders are sent via a UI popup.
 
    .PARAMETER Minutes
    Number of minutes before the start of the event when the reminder should trigger. Defaults to 30 minutes.
 
    Valid values are between 0 and 40320 (4 weeks in minutes).
 
    .PARAMETER InputObject
    Used for pipeline input of an existing IM object to strip the extra attributes and prevent errors
 
    .EXAMPLE
 
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.EventReminder')]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory, ParameterSetName = "Fields")]
        [ValidateSet('email','sms','popup')]
        [String]
        $Method,
        [Parameter(Mandatory, ParameterSetName = "Fields")]
        [Int]
        $Minutes,
        [Parameter(ValueFromPipeline, ParameterSetName = "InputObject")]
        [Google.Apis.Calendar.v3.Data.EventReminder[]]
        $InputObject
    )
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    New-Object 'Google.Apis.Calendar.v3.Data.EventReminder' -Property @{
                        Method = $Method
                        Minutes = $Minutes
                    }
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        New-Object 'Google.Apis.Calendar.v3.Data.EventReminder' -Property @{
                            Method = $iObj.Method
                            Minutes = $iObj.Minutes
                        }
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSCalendarEventReminder'
function Add-GSCalendarNotification {
    <#
    .SYNOPSIS
    Builds an CalendarNotification object to use when creating or updating a CalendarSubscription
 
    .DESCRIPTION
    Builds an CalendarNotification object to use when creating or updating a CalendarSubscription
 
    .PARAMETER Method
    The method used to deliver the notification.
 
    Possible values are:
    * "email" - Reminders are sent via email.
    * "sms" - Reminders are sent via SMS. This value is read-only and is ignored on inserts and updates. SMS reminders are only available for G Suite customers.
 
    .PARAMETER Type
    The type of notification.
 
    Possible values are:
    * "eventCreation" - Notification sent when a new event is put on the calendar.
    * "eventChange" - Notification sent when an event is changed.
    * "eventCancellation" - Notification sent when an event is cancelled.
    * "eventResponse" - Notification sent when an event is changed.
    * "agenda" - An agenda with the events of the day (sent out in the morning).
 
    .PARAMETER InputObject
    Used for pipeline input of an existing IM object to strip the extra attributes and prevent errors
 
    .EXAMPLE
 
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.CalendarNotification')]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory, ParameterSetName = "Fields")]
        [ValidateSet('email','sms')]
        [String]
        $Method,
        [Parameter(Mandatory, ParameterSetName = "Fields")]
        [ValidateSet('eventCreation','eventChange','eventCancellation','eventResponse','agenda')]
        [String]
        $Type,
        [Parameter(ValueFromPipeline, ParameterSetName = "InputObject")]
        [Google.Apis.Calendar.v3.Data.CalendarNotification[]]
        $InputObject
    )
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    New-Object 'Google.Apis.Calendar.v3.Data.CalendarNotification' -Property @{
                        Method = $Method
                        Type = $Type
                    }
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        New-Object 'Google.Apis.Calendar.v3.Data.CalendarNotification' -Property @{
                            Method = $iObj.Method
                            Type = $iObj.Type
                        }
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSCalendarNotification'
function Add-GSChatButton {
    <#
    .SYNOPSIS
    Creates a Chat Button widget to include in a section
 
    .DESCRIPTION
    Creates a Chat Button widget to include in a section
 
    .PARAMETER Text
    The Text for a Text Button
 
    .PARAMETER Icon
    The icon for the Image Button
 
    Available values are:
    * AIRPLANE
    * BOOKMARK
    * BUS
    * CAR
    * CLOCK
    * CONFIRMATION_NUMBER_ICON
    * DOLLAR
    * DESCRIPTION
    * EMAIL
    * EVENT_PERFORMER
    * EVENT_SEAT
    * FLIGHT_ARRIVAL
    * FLIGHT_DEPARTURE
    * HOTEL
    * HOTEL_ROOM_TYPE
    * INVITE
    * MAP_PIN
    * MEMBERSHIP
    * MULTIPLE_PEOPLE
    * OFFER
    * PERSON
    * PHONE
    * RESTAURANT_ICON
    * SHOPPING_CART
    * STAR
    * STORE
    * TICKET
    * TRAIN
    * VIDEO_CAMERA
    * VIDEO_PLAY
 
    .PARAMETER IconUrl
    The Url of the icon for the Image Button
 
    .PARAMETER OnClick
    The OnClick event that triggers when a user clicks the KeyValue
 
    You must use the function `Add-GSChatOnClick` to create OnClicks, otherwise this will throw a terminating error.
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    [CmdletBinding(DefaultParameterSetName = "Text")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ParameterSetName = "Text")]
        [String]
        $Text,
        [parameter(Mandatory = $true,ParameterSetName = "Icon")]
        [ValidateSet('AIRPLANE','BOOKMARK','BUS','CAR','CLOCK','CONFIRMATION_NUMBER_ICON','DOLLAR','DESCRIPTION','EMAIL','EVENT_PERFORMER','EVENT_SEAT','FLIGHT_ARRIVAL','FLIGHT_DEPARTURE','HOTEL','HOTEL_ROOM_TYPE','INVITE','MAP_PIN','MEMBERSHIP','MULTIPLE_PEOPLE','OFFER','PERSON','PHONE','RESTAURANT_ICON','SHOPPING_CART','STAR','STORE','TICKET','TRAIN','VIDEO_CAMERA','VIDEO_PLAY')]
        [String]
        $Icon,
        [parameter(Mandatory = $true,ParameterSetName = "IconUrl")]
        [String]
        $IconUrl,
        [parameter(Mandatory = $false)]
        [ValidateScript( {
            $allowedTypes = "PSGSuite.Chat.Message.Card.OnClick"
            if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                $true
            }
            else {
                throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."
            }
        })]
        [Object]
        $OnClick,
        [parameter(Mandatory = $false,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $widgetObject = @{
            Webhook = @{
                buttons = @()
            }
            SDK = (New-Object 'Google.Apis.HangoutsChat.v1.Data.WidgetMarkup' -Property @{
                Buttons = (New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Button]')
            })
        }
        $widgetStack = @()
        switch ($PSCmdlet.ParameterSetName) {
            Text {
                $widgetObject['Webhook']['buttons'] += @{
                    textButton = @{
                        text = $Text
                        onClick = $OnClick['Webhook']
                    }
                }
                $widgetObject['SDK'].Buttons.Add((New-Object 'Google.Apis.HangoutsChat.v1.Data.Button' -Property @{
                    TextButton = (New-Object 'Google.Apis.HangoutsChat.v1.Data.TextButton' -Property @{
                        Text = $Text
                        OnClick = $OnClick['SDK']
                    })
                })) | Out-Null
            }
            Icon {
                $widgetObject['Webhook']['buttons'] += @{
                    imageButton = @{
                        icon = $Icon
                        onClick = $OnClick['Webhook']
                    }
                }
                $widgetObject['SDK'].Buttons.Add((New-Object 'Google.Apis.HangoutsChat.v1.Data.Button' -Property @{
                    ImageButton = (New-Object 'Google.Apis.HangoutsChat.v1.Data.ImageButton' -Property @{
                        Icon = $Icon
                        OnClick = $OnClick['SDK']
                    })
                })) | Out-Null
            }
            IconUrl {
                $widgetObject['Webhook']['buttons'] += @{
                    imageButton = @{
                        iconUrl = $IconUrl
                        onClick = $OnClick['Webhook']
                    }
                }
                $widgetObject['SDK'].Buttons.Add((New-Object 'Google.Apis.HangoutsChat.v1.Data.Button' -Property @{
                    ImageButton = (New-Object 'Google.Apis.HangoutsChat.v1.Data.ImageButton' -Property @{
                        IconUrl = $IconUrl
                        OnClick = $OnClick['SDK']
                    })
                })) | Out-Null
            }
        }
    }
    Process {
        if ($MessageSegment) {
            foreach ($segment in $MessageSegment) {
                if ($segment.PSTypeNames[0] -in @("PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue")) {
                    $widgetStack += $segment
                }
                else {
                    $segment
                }
            }
        }
    }
    End {
        [void]$widgetObject.PSObject.TypeNames.Insert(0,'PSGSuite.Chat.Message.Card.Section.Button')
        if($widgetStack) {
            $newWidgetStack = @()
            for ($i = 0;$i -lt $widgetStack.Count;$i++) {
                if ($i -eq ($widgetStack.Count -1) -and ($widgetStack[$i].PSTypeNames[0] -eq 'PSGSuite.Chat.Message.Card.Section.Button')) {
                    $widgetStack[$i]['Webhook']['buttons'] += $widgetObject['Webhook']['buttons'][0]
                    $widgetStack[$i]['SDK'].Buttons.Add($widgetObject['SDK'].Buttons[0]) | Out-Null
                    $newWidgetStack += $widgetStack[$i]
                }
                elseif ($i -eq ($widgetStack.Count -1)) {
                    $newWidgetStack += $widgetStack[$i]
                    $newWidgetStack += $widgetObject
                }
                else {
                    $newWidgetStack += $widgetStack[$i]
                }
            }
            $newWidgetStack
        }
        else {
            $widgetObject
        }
    }
}
Export-ModuleMember -Function 'Add-GSChatButton'
function Add-GSChatCard {
    <#
    .SYNOPSIS
    Creates a Chat Message Card
 
    .DESCRIPTION
    Creates a Chat Message Card
 
    .PARAMETER HeaderTitle
    The header title of the card
 
    .PARAMETER HeaderSubtitle
    The header subtitle of the card
 
    .PARAMETER HeaderImageStyle
    The header image style of the card
 
    Available values are:
    * IMAGE
    * AVATAR
 
    .PARAMETER HeaderImageUrl
    The header image URL of the card
 
    .PARAMETER CardActions
    The cardActions of the card.
 
    You must use the function `New-GSChatCardAction` to create cardActions, otherwise this will throw a terminating error.
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    If section widgets are passed directly to this function, a new section without a SectionHeader will be created and the widgets will be added to it
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [String]
        $HeaderTitle,
        [parameter(Mandatory = $false)]
        [String]
        $HeaderSubtitle,
        [parameter(Mandatory = $false)]
        [ValidateSet('IMAGE','AVATAR')]
        [String]
        $HeaderImageStyle,
        [parameter(Mandatory = $false)]
        [String]
        $HeaderImageUrl,
        [parameter(Mandatory = $false)]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card.CardAction"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $CardActions,
        [parameter(Mandatory = $false,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript( {
            $allowedTypes = "PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $cardObject = @{
            Webhook = @{}
            SDK = (New-Object 'Google.Apis.HangoutsChat.v1.Data.Card')
        }
        $addlSectionWidgets = @()
        foreach ($key in $PSBoundParameters.Keys) {
            switch ($key) {
                HeaderTitle {
                    if (!$cardObject['Webhook']['header']) {
                        $cardObject['Webhook']['header'] = @{}
                    }
                    $cardObject['Webhook']['header']['title'] = $PSBoundParameters[$key]
                    if (!$cardObject['SDK'].Header) {
                        $cardObject['SDK'].Header = New-Object 'Google.Apis.HangoutsChat.v1.Data.CardHeader'
                    }
                    $cardObject['SDK'].Header.Title = $PSBoundParameters[$key]
                }
                HeaderSubtitle {
                    if (!$cardObject['Webhook']['header']) {
                        $cardObject['Webhook']['header'] = @{}
                    }
                    $cardObject['Webhook']['header']['subtitle'] = $PSBoundParameters[$key]
                    if (!$cardObject['SDK'].Header) {
                        $cardObject['SDK'].Header = New-Object 'Google.Apis.HangoutsChat.v1.Data.CardHeader'
                    }
                    $cardObject['SDK'].Header.Subtitle = $PSBoundParameters[$key]
                }
                HeaderImageStyle {
                    if (!$cardObject['Webhook']['header']) {
                        $cardObject['Webhook']['header'] = @{}
                    }
                    $cardObject['Webhook']['header']['imageStyle'] = $PSBoundParameters[$key]
                    if (!$cardObject['SDK'].Header) {
                        $cardObject['SDK'].Header = New-Object 'Google.Apis.HangoutsChat.v1.Data.CardHeader'
                    }
                    $cardObject['SDK'].Header.ImageStyle = $PSBoundParameters[$key]
                }
                HeaderImageUrl {
                    if (!$cardObject['Webhook']['header']) {
                        $cardObject['Webhook']['header'] = @{}
                    }
                    $cardObject['Webhook']['header']['imageUrl'] = $PSBoundParameters[$key]
                    if (!$cardObject['SDK'].Header) {
                        $cardObject['SDK'].Header = New-Object 'Google.Apis.HangoutsChat.v1.Data.CardHeader'
                    }
                    $cardObject['SDK'].Header.ImageUrl = $PSBoundParameters[$key]
                }
                CardActions {
                    if (!$cardObject['Webhook']['cardActions']) {
                        $cardObject['Webhook']['cardActions'] = @()
                    }
                    foreach ($cardAction in $CardActions) {
                        $cardObject['Webhook']['cardActions'] += $cardAction['Webhook']
                    }
                    if (!$cardObject['SDK'].CardActions) {
                        $cardObject['SDK'].CardActions = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.CardAction]'
                    }
                    foreach ($cardAction in $CardActions) {
                        $cardObject['SDK'].CardActions.Add($cardAction['SDK']) | Out-Null
                    }
                }
            }
        }
    }
    Process {
        foreach ($segment in $MessageSegment) {
            if ($segment.PSTypeNames[0] -eq 'PSGSuite.Chat.Message.Card') {
                $segment
            }
            elseif ($segment.PSTypeNames[0] -in @("PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue")) {
                $addlSectionWidgets += $segment
            }
            elseif ($segment.PSTypeNames[0] -eq 'PSGSuite.Chat.Message.Card.CardAction') {
                if (!$cardObject['Webhook']['cardActions']) {
                    $cardObject['Webhook']['cardActions'] = @()
                }
                $cardObject['Webhook']['cardActions'] += $segment['Webhook']
                if (!$cardObject['SDK'].CardActions) {
                    $cardObject['SDK'].CardActions = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.CardAction]'
                }
                $cardObject['SDK'].CardActions.Add($segment['SDK']) | Out-Null
            }
            elseif ($segment.PSTypeNames[0] -eq 'PSGSuite.Chat.Message.Card.Section') {
                if (!$cardObject['Webhook']['sections']) {
                    $cardObject['Webhook']['sections'] = @()
                }
                $cardObject['Webhook']['sections'] += $segment['Webhook']
                if (!$cardObject['SDK'].Sections) {
                    $cardObject['SDK'].Sections = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Section]'
                }
                $cardObject['SDK'].Sections.Add($segment['SDK']) | Out-Null
            }
        }
    }
    End {
        if($addlSectionWidgets) {
            $newWidgetStack = @()
            for ($i = 0;$i -lt $addlSectionWidgets.Count;$i++) {
                if ($newWidgetStack -and ($addlSectionWidgets[$i].PSTypeNames[0] -eq 'PSGSuite.Chat.Message.Card.Section.Button') -and ($newWidgetStack[-1].PSTypeNames[0] -eq 'PSGSuite.Chat.Message.Card.Section.Button')) {
                    $newWidgetStack[-1]['Webhook']['buttons'] += $addlSectionWidgets[$i]['Webhook']['buttons'][0]
                    $newWidgetStack[-1]['SDK'].Buttons.Add($addlSectionWidgets[$i]['SDK'].Buttons[0]) | Out-Null
                }
                else {
                    $newWidgetStack += $addlSectionWidgets[$i]
                }
            }
            $addlSection = $newWidgetStack | Add-GSChatCardSection
            if (!$cardObject['Webhook']['sections']) {
                $cardObject['Webhook']['sections'] = @()
            }
            $cardObject['Webhook']['sections'] += $addlSection['Webhook']
            if (!$cardObject['SDK'].Sections) {
                $cardObject['SDK'].Sections = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.Section]'
            }
            $cardObject['SDK'].Sections.Add($addlSection['SDK']) | Out-Null
        }
        [void]$cardObject.PSObject.TypeNames.Insert(0,'PSGSuite.Chat.Message.Card')
        return $cardObject
    }
}
Export-ModuleMember -Function 'Add-GSChatCard'
function Add-GSChatCardAction {
    <#
    .SYNOPSIS
    Creates a Chat CardAction
 
    .DESCRIPTION
    Creates a Chat CardAction
 
    .PARAMETER ActionLabel
    The label used to be displayed in the action menu item.
 
    .PARAMETER OnClick
    The OnClick event that triggers when a user clicks the CardAction
 
    You must use the function `Add-GSChatOnClick` to create OnClicks, otherwise this will throw a terminating error.
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $ActionLabel,
        [parameter(Mandatory = $true,Position = 1)]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card.OnClick"
            if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                $true
            }
            else {
                throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."
            }
        })]
        [Object]
        $OnClick,
        [parameter(Mandatory = $false,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $cardActionObject = @{
            Webhook = @{
                actionLabel = $ActionLabel
                onClick = $OnClick['Webhook']
            }
            SDK = (New-Object 'Google.Apis.HangoutsChat.v1.Data.CardAction' -Property @{
                ActionLabel = $ActionLabel
                OnClick = $OnClick['SDK']
            })
        }
        $widgetStack = @()
    }
    Process {
        foreach ($segment in $MessageSegment) {
            if ($segment.PSTypeNames[0] -in @("PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue")) {
                $widgetStack += $segment
            }
            else {
                $segment
            }
        }
    }
    End {
        [void]$cardActionObject.PSObject.TypeNames.Insert(0,'PSGSuite.Chat.Message.Card.CardAction')
        if($widgetStack) {
            $widgetStack += $cardActionObject
            $widgetStack
        }
        else {
            $cardActionObject
        }
    }
}
Export-ModuleMember -Function 'Add-GSChatCardAction'
function Add-GSChatCardSection {
    <#
    .SYNOPSIS
    Creates a Chat Message Card Section
 
    .DESCRIPTION
    Creates a Chat Message Card Section
 
    .PARAMETER SectionHeader
    The header title of the section
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [String]
        $SectionHeader,
        [parameter(Mandatory = $true,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $sectionObject = @{
            Webhook = @{
                widgets = @()
            }
            SDK = (New-Object 'Google.Apis.HangoutsChat.v1.Data.Section' -Property @{
                Widgets = (New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.WidgetMarkup]')
            })
        }
        if ($PSBoundParameters.Keys -contains 'SectionHeader') {
            $sectionObject['Webhook']['header'] = $SectionHeader
            $sectionObject['SDK'].Header = $SectionHeader
        }
    }
    Process {
        foreach ($segment in $MessageSegment) {
            if ($segment.PSTypeNames[0] -in @("PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue")) {
                $sectionObject['Webhook']['widgets'] += $segment['Webhook']
                $sectionObject['SDK'].Widgets.Add($segment['SDK'])
            }
            else {
                $segment
            }
            
        }
    }
    End {
        [void]$sectionObject.PSObject.TypeNames.Insert(0,'PSGSuite.Chat.Message.Card.Section')
        return $sectionObject
    }
}
Export-ModuleMember -Function 'Add-GSChatCardSection'
function Add-GSChatImage {
    <#
    .SYNOPSIS
    Creates a Chat Image widget to include in a section
 
    .DESCRIPTION
    Creates a Chat Image widget to include in a section
 
    .PARAMETER ImageUrl
    The Url of the Image
 
    .PARAMETER AspectRatio
    The AspectRatio of the Image
 
    .PARAMETER LinkImage
    If $true, automatically creates the OnClick event for the image to open the image URL
 
    .PARAMETER OnClick
    The OnClick event that triggers when a user clicks the KeyValue
 
    You must use the function `Add-GSChatOnClick` to create OnClicks, otherwise this will throw a terminating error.
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    [CmdletBinding(DefaultParameterSetName = "LinkImage")]
    Param
    (
        [parameter(Mandatory = $true)]
        [String]
        $ImageUrl,
        [parameter(Mandatory = $false)]
        [Double]
        $AspectRatio,
        [parameter(Mandatory = $false,ParameterSetName = "LinkImage")]
        [Switch]
        $LinkImage,
        [parameter(Mandatory = $false,ParameterSetName = "OnClick")]
        [ValidateScript( {
            $allowedTypes = "PSGSuite.Chat.Message.Card.OnClick"
            if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                $true
            }
            else {
                throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."
            }
        })]
        [Object]
        $OnClick,
        [parameter(Mandatory = $false,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $widgetObject = @{
            Webhook = @{
                image = @{}
            }
            SDK = (New-Object 'Google.Apis.HangoutsChat.v1.Data.WidgetMarkup' -Property @{
                Image = (New-Object 'Google.Apis.HangoutsChat.v1.Data.Image')
            })
        }
        $widgetStack = @()
        foreach ($key in $PSBoundParameters.Keys) {
            switch ($key) {
                ImageUrl {
                    $widgetObject['Webhook']['image']['imageUrl'] = $PSBoundParameters[$key]
                    $widgetObject['SDK'].Image.ImageUrl = $PSBoundParameters[$key]
                }
                AspectRatio {
                    $widgetObject['Webhook']['image']['aspectRatio'] = $PSBoundParameters[$key]
                    $widgetObject['SDK'].Image.AspectRatio = $PSBoundParameters[$key]
                }
                OnClick {
                    $widgetObject['Webhook']['image']['onClick'] = $PSBoundParameters[$key]['Webhook']
                    $widgetObject['SDK'].Image.OnClick = $PSBoundParameters[$key]['SDK']
                }
            }
        }
        if ($LinkImage) {
            $newOnClick = Add-GSChatOnClick -Url $ImageUrl
            $widgetObject['Webhook']['image']['onClick'] = $newOnClick['Webhook']
            $widgetObject['SDK'].Image.OnClick = $newOnClick['SDK']
        }
    }
    Process {
        foreach ($segment in $MessageSegment) {
            if ($segment.PSTypeNames[0] -in @("PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue")) {
                $widgetStack += $segment
            }
            else {
                $segment
            }
        }
    }
    End {
        [void]$widgetObject.PSObject.TypeNames.Insert(0,'PSGSuite.Chat.Message.Card.Section.Image')
        if ($widgetStack) {
            $widgetStack += $widgetObject
            $widgetStack
        }
        else {
            $widgetObject
        }
    }
}
Export-ModuleMember -Function 'Add-GSChatImage'
function Add-GSChatKeyValue {
    <#
    .SYNOPSIS
    Creates a Chat KeyValue widget to include in a section
 
    .DESCRIPTION
    Creates a Chat KeyValue widget to include in a section
 
    .PARAMETER TopLabel
    The TopLabel for the KeyValue
 
    .PARAMETER Content
    The Content for the KeyValue
 
    .PARAMETER BottomLabel
    The BottomLabel for the KeyValue
 
    .PARAMETER Icon
    The icon to display next to the KeyValue
 
    Available values are:
    * AIRPLANE
    * BOOKMARK
    * BUS
    * CAR
    * CLOCK
    * CONFIRMATION_NUMBER_ICON
    * DOLLAR
    * DESCRIPTION
    * EMAIL
    * EVENT_PERFORMER
    * EVENT_SEAT
    * FLIGHT_ARRIVAL
    * FLIGHT_DEPARTURE
    * HOTEL
    * HOTEL_ROOM_TYPE
    * INVITE
    * MAP_PIN
    * MEMBERSHIP
    * MULTIPLE_PEOPLE
    * OFFER
    * PERSON
    * PHONE
    * RESTAURANT_ICON
    * SHOPPING_CART
    * STAR
    * STORE
    * TICKET
    * TRAIN
    * VIDEO_CAMERA
    * VIDEO_PLAY
 
    .PARAMETER IconUrl
    The Url of the icon to display next to the KeyValue
 
    .PARAMETER ContentMultiline
    Whether the content of the KeyValue is multiline or not
 
    .PARAMETER OnClick
    The OnClick event that triggers when a user clicks the KeyValue
 
    You must use the function `Add-GSChatOnClick` to create OnClicks, otherwise this will throw a terminating error.
 
    .PARAMETER Button
    A button to add to the KeyValue
 
    You must use the function `Add-GSChatButton` to create Buttons, otherwise this will throw a terminating error.
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    [CmdletBinding(DefaultParameterSetName = "Icon")]
    Param
    (
        [parameter(Mandatory = $false)]
        [String]
        $TopLabel,
        [parameter(Mandatory = $false)]
        [String]
        $Content,
        [parameter(Mandatory = $false)]
        [String]
        $BottomLabel,
        [parameter(Mandatory = $false,ParameterSetName = "Icon")]
        [ValidateSet('AIRPLANE','BOOKMARK','BUS','CAR','CLOCK','CONFIRMATION_NUMBER_ICON','DOLLAR','DESCRIPTION','EMAIL','EVENT_PERFORMER','EVENT_SEAT','FLIGHT_ARRIVAL','FLIGHT_DEPARTURE','HOTEL','HOTEL_ROOM_TYPE','INVITE','MAP_PIN','MEMBERSHIP','MULTIPLE_PEOPLE','OFFER','PERSON','PHONE','RESTAURANT_ICON','SHOPPING_CART','STAR','STORE','TICKET','TRAIN','VIDEO_CAMERA','VIDEO_PLAY')]
        [String]
        $Icon,
        [parameter(Mandatory = $false,ParameterSetName = "IconUrl")]
        [String]
        $IconUrl,
        [parameter(Mandatory = $false)]
        [Switch]
        $ContentMultiline,
        [parameter(Mandatory = $false)]
        [ValidateScript( {
            $allowedTypes = "PSGSuite.Chat.Message.Card.OnClick"
            if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                $true
            }
            else {
                throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."
            }
        })]
        [Object]
        $OnClick,
        [parameter(Mandatory = $false)]
        [ValidateScript( {
            $allowedTypes = "PSGSuite.Chat.Message.Card.Section.Button"
            if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                $true
            }
            else {
                throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."
            }
        })]
        [Object]
        $Button,
        [parameter(Mandatory = $false,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $widgetObject = @{
            Webhook = @{
                keyValue = @{}
            }
            SDK = (New-Object 'Google.Apis.HangoutsChat.v1.Data.WidgetMarkup' -Property @{
                KeyValue = (New-Object 'Google.Apis.HangoutsChat.v1.Data.KeyValue')
            })
        }
        $widgetStack = @()
        foreach ($key in $PSBoundParameters.Keys) {
            switch ($key) {
                TopLabel {
                    $widgetObject['Webhook']['keyValue']['topLabel'] = $PSBoundParameters[$key]
                    $widgetObject['SDK'].KeyValue.TopLabel = $PSBoundParameters[$key]
                }
                Content {
                    $widgetObject['Webhook']['keyValue']['content'] = $PSBoundParameters[$key]
                    $widgetObject['SDK'].KeyValue.Content = $PSBoundParameters[$key]
                }
                BottomLabel {
                    $widgetObject['Webhook']['keyValue']['bottomLabel'] = $PSBoundParameters[$key]
                    $widgetObject['SDK'].KeyValue.BottomLabel = $PSBoundParameters[$key]
                }
                Icon {
                    $widgetObject['Webhook']['keyValue']['icon'] = $PSBoundParameters[$key]
                    $widgetObject['SDK'].KeyValue.Icon = $PSBoundParameters[$key]
                }
                IconUrl {
                    $widgetObject['Webhook']['keyValue']['iconUrl'] = $PSBoundParameters[$key]
                    $widgetObject['SDK'].KeyValue.IconUrl = $PSBoundParameters[$key]
                }
                ContentMultiline {
                    $widgetObject['Webhook']['keyValue']['contentMultiline'] = $PSBoundParameters[$key]
                    $widgetObject['SDK'].KeyValue.ContentMultiline = $PSBoundParameters[$key]
                }
                OnClick {
                    $widgetObject['Webhook']['keyValue']['onClick'] = $PSBoundParameters[$key]['Webhook']
                    $widgetObject['SDK'].KeyValue.OnClick = $PSBoundParameters[$key]['SDK']
                }
                Button {
                    $widgetObject['Webhook']['keyValue']['button'] = $PSBoundParameters[$key]['Webhook']
                    $widgetObject['SDK'].KeyValue.Button = $PSBoundParameters[$key]['SDK']
                }
            }
        }
    }
    Process {
        foreach ($segment in $MessageSegment) {
            if ($segment.PSTypeNames[0] -in @("PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue")) {
                $widgetStack += $segment
            }
            else {
                $segment
            }
        }
    }
    End {
        [void]$widgetObject.PSObject.TypeNames.Insert(0,'PSGSuite.Chat.Message.Card.Section.KeyValue')
        if ($widgetStack) {
            $widgetStack += $widgetObject
            $widgetStack
        }
        else {
            $widgetObject
        }
    }
}
Export-ModuleMember -Function 'Add-GSChatKeyValue'
function Add-GSChatOnClick {
    <#
    .SYNOPSIS
    Creates a Chat OnClick action to include in a widget
 
    .DESCRIPTION
    Creates a Chat OnClick action to include in a widget
 
    .PARAMETER Url
    The Url to open for an OpenLink action on click
 
    .PARAMETER ActionMethodName
    Apps Script function to invoke when the containing element is clicked/activated.
 
    .PARAMETER ActionParameters
    A hashtable containing key/value pairs of parameters to pass to the ActionMethod
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    [CmdletBinding(DefaultParameterSetName = "OpenLink")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ParameterSetName = "OpenLink")]
        [String]
        $Url,
        [parameter(Mandatory = $true,ParameterSetName = "Action")]
        [String]
        $ActionMethodName,
        [parameter(Mandatory = $false,ParameterSetName = "Action")]
        [Hashtable[]]
        $ActionParameters
    )
    Begin {
        $onClickObject = @{
            Webhook = @{}
            SDK = (New-Object 'Google.Apis.HangoutsChat.v1.Data.OnClick')
        }
    }
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            OpenLink {
                $onClickObject['Webhook']['openLink'] = @{
                    url = $Url
                }
                $onClickObject['SDK'].OpenLink = (New-Object 'Google.Apis.HangoutsChat.v1.Data.OpenLink' -Property @{
                    Url = $Url
                })
            }
            Action {
                $onClickObject['Webhook']['action'] = @{
                    actionMethodName = $ActionMethodName
                }
                $onClickObject['SDK'].Action = (New-Object 'Google.Apis.HangoutsChat.v1.Data.FormAction' -Property @{
                    ActionMethodName = $ActionMethodName
                })
                if ($PSBoundParameters.Keys -contains 'ActionParameters') {
                    $onClickObject['Webhook']['action']['parameters'] = @()
                    $onClickObject['SDK'].Action.Parameters = New-Object 'System.Collections.Generic.List[Google.Apis.HangoutsChat.v1.Data.ActionParameter]'
                    foreach ($dict in $ActionParameters) {
                        if ($dict.Keys.Count -eq 2 -and $dict.Keys -contains 'key' -and $dict.Keys -contains 'value') {
                            $onClickObject['Webhook']['action']['parameters'] += ([PSCustomObject]@{
                                key = "$($dict['key'])"
                                value = "$($dict['value'])"
                            })
                            $onClickObject['SDK'].Action.Parameters.Add((New-Object 'Google.Apis.HangoutsChat.v1.Data.ActionParameter' -Property @{
                                Key = $dict['key']
                                Value = $dict['value']
                            })) | Out-Null
                        }
                        else {
                            foreach ($key in $dict.Keys) {
                                $onClickObject['Webhook']['action']['parameters'] += ([PSCustomObject]@{
                                    key = "$key"
                                    value = "$($dict[$key])"
                                })
                                $onClickObject['SDK'].Action.Parameters.Add((New-Object 'Google.Apis.HangoutsChat.v1.Data.ActionParameter' -Property @{
                                    Key = $key
                                    Value = $dict[$key]
                                })) | Out-Null
                            }
                        }
                    }
                }
            }
        }
    }
    End {
        [void]$onClickObject.PSObject.TypeNames.Insert(0,'PSGSuite.Chat.Message.Card.OnClick')
        $onClickObject
    }
}
Export-ModuleMember -Function 'Add-GSChatOnClick'
function Add-GSChatTextParagraph {
    <#
    .SYNOPSIS
    Creates a Chat TextParagraph widget to include in a section
 
    .DESCRIPTION
    Creates a Chat TextParagraph widget to include in a section
 
    .PARAMETER Text
    The text for the textParagraph
 
    .PARAMETER MessageSegment
    Any Chat message segment objects created with functions named `Add-GSChat*` passed through the pipeline or added directly to this parameter as values.
 
    .EXAMPLE
    Send-GSChatMessage -Text "Post job report:" -Cards $cards -Webhook (Get-GSChatWebhook JobReports)
 
    Sends a simple Chat message using the JobReports webhook
 
    .EXAMPLE
    Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>crap</b>!" |
    Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
    Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
    Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
    Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" -OutVariable sect1 |
    Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -Url "https://github.com/scrthq/PSGSuite") -Verbose -OutVariable button1 |
    Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -Url "https://admin.google.com/?hl=en&authuser=0") -Verbose -OutVariable button2 |
    Add-GSChatCardSection -SectionHeader "What should we do?" -OutVariable sect2 |
    Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
    Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b> via WebHook!" |
    Add-GSChatCardSection -SectionHeader "Additional Info" -OutVariable sect2 |
    Send-GSChatMessage -Text "Got that report, boss:" -FallbackText "Mistakes have been made..." -Webhook ReportRoom
 
    This example shows the pipeline capabilities of the Chat functions in PSGSuite. Starting from top to bottom:
        1. Add a TextParagraph widget
        2. Add a KeyValue with an icon
        3. Add another KeyValue with a different icon
        4. Add an image and create an OnClick event to open the image's URL by using the -LinkImage parameter
        5. Add a new section to encapsulate the widgets sent through the pipeline before it
        6. Add a TextButton that opens the PSGSuite GitHub repo when clicked
        7. Add another TextButton that opens Google Admin Console when clicked
        8. Wrap the 2 buttons in a new Section to divide the content
        9. Wrap all widgets and sections in the pipeline so far in a Card
        10. Add a new TextParagraph as a footer to the message
        11. Wrap that TextParagraph in a new section
        12. Send the message and include FallbackText that's displayed in the mobile notification. Since the final TextParagraph and Section are not followed by a new Card addition, Send-GSChatMessage will create a new Card just for the remaining segments then send the completed message via Webhook. The Webhook short-name is used to reference the full URL stored in the encrypted Config so it's not displayed in the actual script.
 
    .EXAMPLE
    Get-Service | Select-Object -First 5 | ForEach-Object {
        Add-GSChatKeyValue -TopLabel $_.DisplayName -Content $_.Status -BottomLabel $_.Name -Icon TICKET
    } | Add-GSChatCardSection -SectionHeader "Top 5 Services" | Send-GSChatMessage -Text "Service Report:" -FallbackText "Service Report" -Webhook Reports
 
    This gets the first 5 Services returned by Get-Service, creates KeyValue widgets for each, wraps it in a section with a header, then sends it to the Reports Webhook
    #>

    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String[]]
        $Text,
        [parameter(Mandatory = $false,ValueFromPipeline = $true)]
        [Alias('InputObject')]
        [ValidateScript({
            $allowedTypes = "PSGSuite.Chat.Message.Card.Section","PSGSuite.Chat.Message.Card","PSGSuite.Chat.Message.Card.CardAction","PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue"
            foreach ($item in $_) {
                if ([string]$($item.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    throw "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($item.PSTypeNames -join ", ")."
                }
            }
        })]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $widgetObject = @{
            Webhook = @{
                textParagraph = @{
                    text = ($Text -join "`n")
                }
            }
            SDK = (New-Object 'Google.Apis.HangoutsChat.v1.Data.WidgetMarkup' -Property @{
                TextParagraph = (New-Object 'Google.Apis.HangoutsChat.v1.Data.TextParagraph' -Property @{
                    Text = ($Text -join "`n")
                })
            })
        }
        $widgetStack = @()
    }
    Process {
        foreach ($segment in $MessageSegment) {
            if ($segment.PSTypeNames[0] -in @("PSGSuite.Chat.Message.Card.Section.TextParagraph","PSGSuite.Chat.Message.Card.Section.Button","PSGSuite.Chat.Message.Card.Section.Image","PSGSuite.Chat.Message.Card.Section.KeyValue")) {
                $widgetStack += $segment
            }
            else {
                $segment
            }
        }
    }
    End {
        [void]$widgetObject.PSObject.TypeNames.Insert(0,'PSGSuite.Chat.Message.Card.Section.TextParagraph')
        if ($widgetStack) {
            $widgetStack += $widgetObject
            $widgetStack
        }
        else {
            $widgetObject
        }
    }
}
Export-ModuleMember -Function 'Add-GSChatTextParagraph'
function Add-GSCustomerPostalAddress {
    <#
    .SYNOPSIS
    Builds a PostalAddress object to use when creating or updating a Customer
 
    .DESCRIPTION
    Builds a PostalAddress object to use when creating or updating a Customer
 
    .PARAMETER AddressLine1
    A customer's physical address. The address can be composed of one to three lines.
 
    .PARAMETER AddressLine2
    Address line 2 of the address.
 
    .PARAMETER AddressLine3
    Address line 3 of the address.
 
    .PARAMETER ContactName
    The customer contact's name.
 
    .PARAMETER CountryCode
    The country code. Uses the ISO 3166-1 standard: http://www.iso.org/iso/iso-3166-1_decoding_table
 
    .PARAMETER Locality
    Name of the locality. An example of a locality value is the city of San Francisco.
 
    .PARAMETER OrganizationName
    The company or company division name.
 
    .PARAMETER PostalCode
    The postal code. A postalCode example is a postal zip code such as 10009. This is in accordance with - http://portablecontacts.net/draft-spec.html#address_element.
 
    .PARAMETER Region
    Name of the region. An example of a region value is NY for the state of New York.
 
    .PARAMETER InputObject
    Used for pipeline input of an existing UserAddress object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    Add-GSCustomerPostalAddress -AddressLine1 '123 Front St' -AddressLine2 'Los Angeles, CA 90210' -ContactName 'Jim'
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.CustomerPostalAddress')]
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $AddressLine1,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $AddressLine2,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $AddressLine3,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $ContactName,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CountryCode,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Alias('Town', 'City')]
        [String]
        $Locality,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $OrganizationName,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $PostalCode,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Alias('State', 'Province')]
        [String]
        $Region,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.CustomerPostalAddress]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'AddressLine1'
            'AddressLine2'
            'AddressLine3'
            'ContactName'
            'CountryCode'
            'Locality'
            'OrganizationName'
            'PostalCode'
            'Region'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.CustomerPostalAddress'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        $obj.$prop = $PSBoundParameters[$prop]
                    }
                    $obj
                }
                InputObject {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.CustomerPostalAddress'
                    foreach ($prop in $InputObject.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                        $obj.$prop = $InputObject.$prop
                    }
                    $obj
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSCustomerPostalAddress'
function Add-GSEventAttendee {
    <#
    .SYNOPSIS
    Adds an event attendee to a calendar event
 
    .DESCRIPTION
    Adds an event attendee to a calendar event
 
    .PARAMETER Email
    The email address of the attendee
 
    .PARAMETER AdditionalGuests
    How many additional guests, if any
 
    .PARAMETER Comment
    Attendee comment
 
    .PARAMETER DisplayName
    The attendee's name, if available
 
    .PARAMETER Optional
    Whether this is an optional attendee
 
    .PARAMETER Organizer
    Whether the attendee is the organizer of the event
 
    .PARAMETER Resource
    Whether the attendee is a resource
 
    .PARAMETER ResponseStatus
    The attendee's response status.
 
    Possible values are:
    * "NeedsAction": The attendee has not responded to the invitation.
    * "Declined": The attendee has declined the invitation.
    * "Tentative": The attendee has tentatively accepted the invitation.
    * "Accepted": The attendee has accepted the invitation
 
    .PARAMETER InputObject
    Used for pipeline input of an existing UserAddress object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    Add-GSEventAttendee -Email 'joe@domain.com'
    #>

    [OutputType('Google.Apis.Calendar.v3.Data.EventAttendee')]
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    Param
    (
        [Parameter(Mandatory = $true,ParameterSetName = "Fields")]
        [String]
        $Email,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [Int]
        $AdditionalGuests,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [String]
        $Comment,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [String]
        $DisplayName,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [Switch]
        $Optional,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [Switch]
        $Organizer,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [Switch]
        $Resource,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [ValidateSet('NeedsAction','Declined','Tentative','Accepted')]
        [String]
        $ResponseStatus,
        [Parameter(Mandatory = $false,ValueFromPipeline = $true,ParameterSetName = "InputObject")]
        [Google.Apis.Calendar.v3.Data.EventAttendee[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'AdditionalGuests'
            'Comment'
            'DisplayName'
            'Email'
            'Optional'
            'Organizer'
            'Resource'
            'ResponseStatus'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    Write-Verbose "Adding event attendee '$Email'"
                    $obj = New-Object 'Google.Apis.Calendar.v3.Data.EventAttendee'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        $obj.$prop = $PSBoundParameters[$prop]
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Calendar.v3.Data.EventAttendee'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSEventAttendee'
function Add-GSGmailSmtpMsa {
    <#
    .SYNOPSIS
    Builds a SmtpMsa object to use when creating or updating SmtpMsa settings withing the Gmail SendAs settings.
 
    .DESCRIPTION
    Builds a SmtpMsa object to use when creating or updating SmtpMsa settings withing the Gmail SendAs settings.
 
    .PARAMETER HostName
    The hostname of the SMTP service.
 
    .PARAMETER Port
    The port of the SMTP service.
 
    .PARAMETER SecurityMode
    The protocol that will be used to secure communication with the SMTP service.
 
    Acceptable values are:
    * "none"
    * "securityModeUnspecified"
    * "ssl"
    * "starttls"
 
    .PARAMETER Username
    The username that will be used for authentication with the SMTP service.
 
    This is a write-only field that can be specified in requests to create or update SendAs settings; it is never populated in responses.
 
    .PARAMETER Password
    The password that will be used for authentication with the SMTP service.
 
    This is a write-only field that can be specified in requests to create or update SendAs settings; it is never populated in responses.
 
    .PARAMETER TreatAsAlias
    Whether Gmail should treat this address as an alias for the user's primary email address. This setting only applies to custom "from" aliases.
 
    .PARAMETER InputObject
    Used for pipeline input of an existing UserAddress object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    $smtpMsa = Add-GSGmailSmtpMsa -Host 10.0.30.18 -Port 3770 -SecurityMode none -Username mailadmin -Password $(ConvertTo-SecureString $password -AsPlainText -Force)
    Update-GSGmailSendAsSettings -SendAsEmail joseph.wiggum@business.com -User joe@domain.com -Signature "<div>Thank you for your time,</br>Joseph Wiggum</div>" -SmtpMsa $smtpMsa
 
    Updates Joe's SendAs settings for his work SendAs alias, including signature and SmtpMsa settings.
    #>

    [OutputType('Google.Apis.Gmail.v1.Data.SmtpMsa')]
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    Param
    (
        [Parameter(Mandatory = $true, ParameterSetName = "Fields")]
        [Alias('Host')]
        [string]
        $HostName,
        [Parameter(Mandatory = $true, ParameterSetName = "Fields")]
        [int]
        $Port,
        [Parameter(Mandatory = $true, ParameterSetName = "Fields")]
        [ValidateSet('none','securityModeUnspecified','ssl','starttls')]
        [String]
        $SecurityMode,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Username,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [SecureString]
        $Password,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Gmail.v1.Data.SmtpMsa[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'HostName'
            'Port'
            'SecurityMode'
            'Username'
            'Password'
            'TreatAsAlias'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Gmail.v1.Data.SmtpMsa'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        $obj.$prop = $PSBoundParameters[$prop]
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Gmail.v1.Data.SmtpMsa'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSGmailSmtpMsa'
function Add-GSUserAddress {
    <#
    .SYNOPSIS
    Builds a UserAddress object to use when creating or updating a User
 
    .DESCRIPTION
    Builds a UserAddress object to use when creating or updating a User
 
    .PARAMETER Country
    Country
 
    .PARAMETER CountryCode
    The country code. Uses the ISO 3166-1 standard: http://www.iso.org/iso/iso-3166-1_decoding_table
 
    .PARAMETER CustomType
    If the address type is custom, this property contains the custom value
 
    .PARAMETER ExtendedAddress
    For extended addresses, such as an address that includes a sub-region
 
    .PARAMETER Formatted
    A full and unstructured postal address. This is not synced with the structured address fields
 
    .PARAMETER Locality
    The town or city of the address
 
    .PARAMETER PoBox
    The post office box, if present
 
    .PARAMETER PostalCode
    The ZIP or postal code, if applicable
 
    .PARAMETER Primary
    If this is the user's primary address. The addresses list may contain only one primary address
 
    .PARAMETER Region
    The abbreviated province or state
 
    .PARAMETER SourceIsStructured
    Indicates if the user-supplied address was formatted. Formatted addresses are not currently supported
 
    .PARAMETER StreetAddress
    The street address, such as 1600 Amphitheatre Parkway. Whitespace within the string is ignored; however, newlines are significant
 
    .PARAMETER Type
    The address type.
 
    Acceptable values are:
    * "custom"
    * "home"
    * "other"
    * "work"
 
    .PARAMETER InputObject
    Used for pipeline input of an existing UserAddress object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    $address = Add-GSUserAddress -Country USA -Locality Dallas -PostalCode 75000 Region TX -StreetAddress '123 South St' -Type Work -Primary
 
    $phone = Add-GSUserPhone -Type Work -Value "(800) 873-0923" -Primary
 
    $extId = Add-GSUserExternalId -Type Login_Id -Value jsmith2
 
    $email = Add-GSUserEmail -Type work -Address jsmith@contoso.com
 
    New-GSUser -PrimaryEmail john.smith@domain.com -GivenName John -FamilyName Smith -Password (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) -ChangePasswordAtNextLogin -OrgUnitPath "/Users/New Hires" -IncludeInGlobalAddressList -Addresses $address -Phones $phone -ExternalIds $extId -Emails $email
 
    Creates a user named John Smith and adds their work address, work phone, login_id and alternate non gsuite work email to the user object.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserAddress')]
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Country,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CountryCode,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomType,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $ExtendedAddress,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Formatted,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Alias('Town', 'City')]
        [String]
        $Locality,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $PoBox,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $PostalCode,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Switch]
        $Primary,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Alias('State', 'Province')]
        [String]
        $Region,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Switch]
        $SourceIsStructured,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $StreetAddress,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [ValidateSet('custom', 'home', 'other', 'work')]
        [String]
        $Type,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserAddress[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'Country'
            'CountryCode'
            'CustomType'
            'ExtendedAddress'
            'Formatted'
            'Locality'
            'PoBox'
            'PostalCode'
            'Primary'
            'Region'
            'SourceIsStructured'
            'StreetAddress'
            'Type'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserAddress'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        if ($prop -eq 'Type') {
                            $obj.$prop = $PSBoundParameters[$prop].ToLower()
                        }
                        else {
                            $obj.$prop = $PSBoundParameters[$prop]
                        }
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserAddress'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserAddress'
function Add-GSUserEmail {
    <#
    .SYNOPSIS
    Builds a Email object to use when creating or updating a User
 
    .DESCRIPTION
    Builds a Email object to use when creating or updating a User
 
    .PARAMETER Address
    The user's email address. Also serves as the email ID. This value can be the user's primary email address or an alias.
 
    .PARAMETER CustomType
    If the value of type is custom, this property contains the custom type.
 
    .PARAMETER Primary
    Indicates if this is the user's primary email. Only one entry can be marked as primary.
 
    .PARAMETER Type
    The type of the email account.
 
    Acceptable values are:
    * "custom"
    * "home"
    * "other"
    * "work"
 
    .PARAMETER InputObject
    Used for pipeline input of an existing Email object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    $address = Add-GSUserAddress -Country USA -Locality Dallas -PostalCode 75000 Region TX -StreetAddress '123 South St' -Type Work -Primary
 
    $phone = Add-GSUserPhone -Type Work -Value "(800) 873-0923" -Primary
 
    $extId = Add-GSUserExternalId -Type Login_Id -Value jsmith2
 
    $email = Add-GSUserEmail -Type work -Address jsmith@contoso.com
 
    New-GSUser -PrimaryEmail john.smith@domain.com -GivenName John -FamilyName Smith -Password (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) -ChangePasswordAtNextLogin -OrgUnitPath "/Users/New Hires" -IncludeInGlobalAddressList -Addresses $address -Phones $phone -ExternalIds $extId -Emails $email
 
    Creates a user named John Smith and adds their work address, work phone, login_id and alternate non gsuite work email to the user object.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserEmail')]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Address,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomType,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Switch]
        $Primary,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [ValidateSet('custom', 'home', 'other', 'work')]
        [String]
        $Type,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserEmail[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'Address'
            'CustomType'
            'Type'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserEmail'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        if ($prop -eq 'Type') {
                            $obj.$prop = $PSBoundParameters[$prop].ToLower()
                        }
                        else {
                            $obj.$prop = $PSBoundParameters[$prop]
                        }
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserEmail'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserEmail'
function Add-GSUserExternalId {
    <#
    .SYNOPSIS
    Builds a UserExternalId object to use when creating or updating a User
 
    .DESCRIPTION
    Builds a UserExternalId object to use when creating or updating a User
 
    .PARAMETER CustomType
    If the external ID type is custom, this property holds the custom type
 
    .PARAMETER Type
    The type of the ID.
 
    Acceptable values are:
    * "account"
    * "custom"
    * "customer"
    * "login_id"
    * "network"
    * "organization": For example, Employee ID.
 
    .PARAMETER Value
    The value of the ID
 
    .PARAMETER InputObject
    Used for pipeline input of an existing UserExternalId object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    $address = Add-GSUserAddress -Country USA -Locality Dallas -PostalCode 75000 Region TX -StreetAddress '123 South St' -Type Work -Primary
 
    $phone = Add-GSUserPhone -Type Work -Value "(800) 873-0923" -Primary
 
    $extId = Add-GSUserExternalId -Type Login_Id -Value jsmith2
 
    $email = Add-GSUserEmail -Type work -Address jsmith@contoso.com
 
    New-GSUser -PrimaryEmail john.smith@domain.com -GivenName John -FamilyName Smith -Password (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) -ChangePasswordAtNextLogin -OrgUnitPath "/Users/New Hires" -IncludeInGlobalAddressList -Addresses $address -Phones $phone -ExternalIds $extId -Emails $email
 
    Creates a user named John Smith and adds their work address, work phone, login_id and alternate non gsuite work email to the user object.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId')]
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomType,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [ValidateSet('account', 'custom', 'customer', 'login_id', 'network', 'organization')]
        [String]
        $Type,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Alias('ExternalId')]
        [String]
        $Value,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'CustomType'
            'Type'
            'Value'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        if ($prop -eq 'Type') {
                            $obj.$prop = $PSBoundParameters[$prop].ToLower()
                        }
                        else {
                            $obj.$prop = $PSBoundParameters[$prop]
                        }
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserExternalId'
function Add-GSUserIm {
    <#
    .SYNOPSIS
    Builds an IM object to use when creating or updating a User
 
    .DESCRIPTION
    Builds an IM object to use when creating or updating a User
 
    .PARAMETER CustomProtocol
    If the protocol value is custom_protocol, this property holds the custom protocol's string.
 
    .PARAMETER CustomType
    If the value of type is custom, this property contains the custom type.
 
    .PARAMETER Im
    The user's IM network ID.
 
    .PARAMETER Primary
    If this is the user's primary IM. Only one entry in the IM list can have a value of true.
 
    .PARAMETER Protocol
    An IM protocol identifies the IM network. The value can be a custom network or the standard network.
 
    Acceptable values are:
    * "aim": AOL Instant Messenger protocol
    * "custom_protocol": A custom IM network protocol
    * "gtalk": Google Talk protocol
    * "icq": ICQ protocol
    * "jabber": Jabber protocol
    * "msn": MSN Messenger protocol
    * "net_meeting": Net Meeting protocol
    * "qq": QQ protocol
    * "skype": Skype protocol
    * "yahoo": Yahoo Messenger protocol
 
 
"aim","custom_protocol","gtalk","icq","jabber","msn","net_meeting","qq","skype","yahoo"
 
    .PARAMETER Type
    The type of the IM account.
 
    Acceptable values are:
    * "custom"
    * "home"
    * "other"
    * "work"
 
    .PARAMETER InputObject
    Used for pipeline input of an existing IM object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    $address = Add-GSUserAddress -Country USA -Locality Dallas -PostalCode 75000 Region TX -StreetAddress '123 South St' -Type Work -Primary
 
    $phone = Add-GSUserPhone -Type Work -Value "(800) 873-0923" -Primary
 
    $extId = Add-GSUserExternalId -Type Login_Id -Value jsmith2
 
    $email = Add-GSUserEmail -Type work -Address jsmith@contoso.com
 
    $im = Add-GSUserIm -Type work -Protocol custom_protocol -CustomProtocol spark -Im jsmithertons100
 
    New-GSUser -PrimaryEmail john.smith@domain.com -GivenName John -FamilyName Smith -Password (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) -ChangePasswordAtNextLogin -OrgUnitPath "/Users/New Hires" -IncludeInGlobalAddressList -Addresses $address -Phones $phone -ExternalIds $extId -Emails $email -Ims $im
 
    Creates a user named John Smith and adds their work address, work phone, IM, login_id and alternate non gsuite work email to the user object.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserIm')]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomProtocol,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomType,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Im,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Switch]
        $Primary,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [ValidateSet("aim","custom_protocol","gtalk","icq","jabber","msn","net_meeting","qq","skype","yahoo")]
        [String]
        $Protocol,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [ValidateSet('custom', 'home', 'other', 'work')]
        [String]
        $Type,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserIm[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'CustomProtocol'
            'CustomType'
            'Im'
            'Primary'
            'Protocol'
            'Type'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserIm'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        if ($prop -in @('Type','Protocol')) {
                            $obj.$prop = $PSBoundParameters[$prop].ToLower()
                        }
                        else {
                            $obj.$prop = $PSBoundParameters[$prop]
                        }
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserIm'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserIm'
function Add-GSUserLocation {
    <#
    .SYNOPSIS
    Builds a Location object to use when creating or updating a User
 
    .DESCRIPTION
    Builds a Location object to use when creating or updating a User
 
    .PARAMETER Area
    Textual location. This is most useful for display purposes to concisely describe the location. For example, "Mountain View, CA", "Near Seattle", "US-NYC-9TH 9A209A"
 
    .PARAMETER BuildingId
    Building Identifier.
 
    .PARAMETER CustomType
    Custom Type.
 
    .PARAMETER DeskCode
    Most specific textual code of individual desk location.
 
    .PARAMETER FloorName
    Floor name/number.
 
    .PARAMETER FloorSection
    Floor section. More specific location within the floor. For example, if a floor is divided into sections "A", "B", and "C", this field would identify one of those values.
 
    .PARAMETER Type
    Each entry can have a type which indicates standard types of that entry. For example location could be of types default and desk. In addition to standard type, an entry can have a custom type and can give it any name. Such types should have "custom" as type and also have a customType value.
 
    Acceptable values are:
    * "custom"
    * "default"
    * "desk"
 
    .PARAMETER InputObject
    Used for pipeline input of an existing Location object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    Add-GSUserLocation -Area "Bellevue, WA" -BuildingId '30' -CustomType "LemonadeStand" -Type custom
 
    Adds a custom user location.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserLocation')]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Area,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $BuildingId,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomType,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $DeskCode,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $FloorName,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $FloorSection,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [ValidateSet('custom', 'default', 'desk')]
        [String]
        $Type,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserLocation[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'Area'
            'BuildingId'
            'CustomType'
            'DeskCode'
            'FloorName'
            'FloorSection'
            'Type'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserLocation'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        if ($prop -eq 'Type') {
                            $obj.$prop = $PSBoundParameters[$prop].ToLower()
                        }
                        else {
                            $obj.$prop = $PSBoundParameters[$prop]
                        }
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserLocation'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserLocation'
function Add-GSUserOrganization {
    <#
    .SYNOPSIS
    Builds a Organization object to use when creating or updating a User
 
    .DESCRIPTION
    Builds a Organization object to use when creating or updating a User
 
    .PARAMETER CostCenter
    The cost center of the users department
 
    .PARAMETER CustomType
    If the external ID type is custom, this property holds the custom type
 
    .PARAMETER Department
    Department within the organization
 
    .PARAMETER Description
    Description of the organization
 
    .PARAMETER Domain
    The domain to which the organization belongs to
 
    .PARAMETER FullTimeEquivalent
    The full-time equivalent percent within the organization (100000 = 100%).
 
    .PARAMETER Location
    Location of the organization. This need not be fully qualified address.
 
    .PARAMETER Name
    Name of the organization
 
    .PARAMETER Primary
    If it is the user's primary organization
 
    .PARAMETER Symbol
    Symbol of the organization
 
    .PARAMETER Title
    Title (designation) of the user in the organization
 
    .PARAMETER Type
    The type of the organization.
 
    If using a CustomType
 
    .PARAMETER Value
    The value of the ID
 
    .PARAMETER InputObject
    Used for pipeline input of an existing UserExternalId object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    $address = Add-GSUserAddress -Country USA -Locality Dallas -PostalCode 75000 Region TX -StreetAddress '123 South St' -Type Work -Primary
 
    $phone = Add-GSUserPhone -Type Work -Value "(800) 873-0923" -Primary
 
    $extId = Add-GSUserExternalId -Type Login_Id -Value jsmith2
 
    $email = Add-GSUserEmail -Type work -Address jsmith@contoso.com
 
    New-GSUser -PrimaryEmail john.smith@domain.com -GivenName John -FamilyName Smith -Password (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) -ChangePasswordAtNextLogin -OrgUnitPath "/Users/New Hires" -IncludeInGlobalAddressList -Addresses $address -Phones $phone -ExternalIds $extId -Emails $email
 
    Creates a user named John Smith and adds their work address, work phone, login_id and alternate non gsuite work email to the user object.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization')]
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CostCenter,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomType,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Department,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Description,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Domain,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Int]
        $FullTimeEquivalent,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Location,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Name,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Switch]
        $Primary,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Symbol,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Title,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Type,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'CostCenter'
            'CustomType'
            'Department'
            'Description'
            'Domain'
            'FullTimeEquivalent'
            'Location'
            'Name'
            'Primary'
            'Symbol'
            'Title'
            'Type'
        )
        if ($PSBoundParameters.Keys -contains 'CustomType') {
            $PSBoundParameters['Type'] = 'CUSTOM'
        }
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        $obj.$prop = $PSBoundParameters[$prop]
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserOrganization'
function Add-GSUserPhone {
    <#
    .SYNOPSIS
    Builds a UserPhone object to use when creating or updating a User
 
    .DESCRIPTION
    Builds a UserPhone object to use when creating or updating a User
 
    .PARAMETER CustomType
    If the value of type is custom, this property contains the custom type
 
    .PARAMETER Primary
    Indicates if this is the user's primary phone number. A user may only have one primary phone number
 
    .PARAMETER Type
    The type of phone number.
 
    Acceptable values are:
    * "assistant"
    * "callback"
    * "car"
    * "company_main"
    * "custom"
    * "grand_central"
    * "home"
    * "home_fax"
    * "isdn"
    * "main"
    * "mobile"
    * "other"
    * "other_fax"
    * "pager"
    * "radio"
    * "telex"
    * "tty_tdd"
    * "work"
    * "work_fax"
    * "work_mobile"
    * "work_pager"
 
    .PARAMETER Value
    A human-readable phone number. It may be in any telephone number format
 
    .PARAMETER InputObject
    Used for pipeline input of an existing UserPhone object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    $address = Add-GSUserAddress -Country USA -Locality Dallas -PostalCode 75000 Region TX -StreetAddress '123 South St' -Type Work -Primary
 
    $phone = Add-GSUserPhone -Type Work -Value "(800) 873-0923" -Primary
 
    $extId = Add-GSUserExternalId -Type Login_Id -Value jsmith2
 
    $email = Add-GSUserEmail -Type work -Address jsmith@contoso.com
 
    New-GSUser -PrimaryEmail john.smith@domain.com -GivenName John -FamilyName Smith -Password (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) -ChangePasswordAtNextLogin -OrgUnitPath "/Users/New Hires" -IncludeInGlobalAddressList -Addresses $address -Phones $phone -ExternalIds $extId -Emails $email
 
    Creates a user named John Smith and adds their work address, work phone, login_id and alternate non gsuite work email to the user object.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserAddress')]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomType,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Switch]
        $Primary,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [ValidateSet('assistant', 'callback', 'car', 'company_main', 'custom', 'grand_central', 'home', 'home_fax', 'isdn', 'main', 'mobile', 'other', 'other_fax', 'pager', 'radio', 'telex', 'tty_tdd', 'work', 'work_fax', 'work_mobile', 'work_pager')]
        [String]
        $Type,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [Alias('Phone')]
        [String]
        $Value,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserAddress[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'CustomType'
            'Type'
            'Value'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserPhone'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        if ($prop -eq 'Type') {
                            $obj.$prop = $PSBoundParameters[$prop].ToLower()
                        }
                        else {
                            $obj.$prop = $PSBoundParameters[$prop]
                        }
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserPhone'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserPhone'
function Add-GSUserRelation {
    <#
    .SYNOPSIS
    Builds a Relation object to use when creating or updating a User
 
    .DESCRIPTION
    Builds a Relation object to use when creating or updating a User
 
    .PARAMETER Type
    The type of relation.
 
    Acceptable values are:
    * "admin_assistant"
    * "assistant"
    * "brother"
    * "child"
    * "custom"
    * "domestic_partner"
    * "dotted_line_manager"
    * "exec_assistant"
    * "father"
    * "friend"
    * "manager"
    * "mother"
    * "parent"
    * "partner"
    * "referred_by"
    * "relative"
    * "sister"
    * "spouse"
 
    .PARAMETER Value
    The name of the person the user is related to.
 
    .PARAMETER CustomType
    If the value of `Type` is `custom`, this property contains the custom type.
 
    .PARAMETER InputObject
    Used for pipeline input of an existing UserExternalId object to strip the extra attributes and prevent errors
 
    .EXAMPLE
    $address = Add-GSUserAddress -Country USA -Locality Dallas -PostalCode 75000 Region TX -StreetAddress '123 South St' -Type Work -Primary
 
    $phone = Add-GSUserPhone -Type Work -Value "(800) 873-0923" -Primary
 
    $extId = Add-GSUserExternalId -Type Login_Id -Value jsmith2
 
    $email = Add-GSUserEmail -Type work -Address jsmith@contoso.com
 
    New-GSUser -PrimaryEmail john.smith@domain.com -GivenName John -FamilyName Smith -Password (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) -ChangePasswordAtNextLogin -OrgUnitPath "/Users/New Hires" -IncludeInGlobalAddressList -Addresses $address -Phones $phone -ExternalIds $extId -Emails $email
 
    Creates a user named John Smith and adds their work address, work phone, login_id and alternate non gsuite work email to the user object.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserRelation')]
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    Param
    (
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [ValidateSet("admin_assistant","assistant","brother","child","custom","domestic_partner","dotted_line_manager","exec_assistant","father","friend","manager","mother","parent","partner","referred_by","relative","sister","spouse")]
        [String]
        $Type,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $Value,
        [Parameter(Mandatory = $false, ParameterSetName = "Fields")]
        [String]
        $CustomType,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserRelation[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'CustomType'
            'Type',
            'Value'
        )
        if ($PSBoundParameters.Keys -contains 'CustomType') {
            $PSBoundParameters['Type'] = 'custom'
        }
        $PSBoundParameters['Type'] = $PSBoundParameters['Type'].ToString().ToLower()
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserRelation'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        $obj.$prop = $PSBoundParameters[$prop]
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserRelation'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserRelation'
function Add-GSUserSchemaField {
    <#
    .SYNOPSIS
    Builds a UserPhone object to use when creating or updating a Schema
 
    .DESCRIPTION
    Builds a UserPhone object to use when creating or updating a Schema
 
    .PARAMETER FieldName
    The name of the field
 
    .PARAMETER FieldType
    The type of the field.
 
 
    * Acceptable values are:
    * "BOOL": Boolean values.
    * "DATE": Dates in ISO-8601 format: http://www.w3.org/TR/NOTE-datetime
    * "DOUBLE": Double-precision floating-point values.
    * "EMAIL": Email addresses.
    * "INT64": 64-bit integer values.
    * "PHONE": Phone numbers.
    * "STRING": String values.
 
    .PARAMETER Indexed
    Switch specifying whether the field is indexed or not. Default: true
 
    .PARAMETER MultiValued
    A switch specifying whether this is a multi-valued field or not. Default: false
 
    .PARAMETER ReadAccessType
    Parameter description
 
    .PARAMETER InputObject
    Parameter description
 
    .EXAMPLE
    New-GSUserSchema -SchemaName "SDK" -Fields (Add-GSUserSchemaField -FieldName "string" -FieldType STRING -ReadAccessType ADMINS_AND_SELF),(Add-GSUserSchemaField -FieldName "date" -FieldType DATE -ReadAccessType ADMINS_AND_SELF)
 
    This command will create a schema named "SDK" with two fields, "string" and "date", readable by ADMINS_AND_SELF
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.SchemaFieldSpec')]
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    Param
    (
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [String]
        $FieldName,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [ValidateSet("BOOL","DATE","DOUBLE","EMAIL","INT64","PHONE","STRING")]
        [String]
        $FieldType = "STRING",
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [Switch]
        $Indexed,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [Switch]
        $MultiValued,
        [Parameter(Mandatory = $false,ParameterSetName = "Fields")]
        [ValidateSet("ADMINS_AND_SELF","ALL_DOMAIN_USERS")]
        [String]
        $ReadAccessType = "ADMINS_AND_SELF",
        [Parameter(Mandatory = $false,ValueFromPipeline = $true,ParameterSetName = "InputObject")]
        [Google.Apis.Admin.Directory.directory_v1.Data.SchemaFieldSpec[]]
        $InputObject
    )
    Begin {
        $propsToWatch = @(
            'FieldName'
            'FieldType'
            'Indexed'
            'MultiValued'
            'ReadAccessType'
        )
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Fields {
                    $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.SchemaFieldSpec'
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object {$obj.PSObject.Properties.Name -contains $_}) {
                        $obj.$prop = $PSBoundParameters[$prop]
                    }
                    $obj
                }
                InputObject {
                    foreach ($iObj in $InputObject) {
                        $obj = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.SchemaFieldSpec'
                        foreach ($prop in $iObj.PSObject.Properties.Name | Where-Object {$obj.PSObject.Properties.Name -contains $_ -and $propsToWatch -contains $_}) {
                            $obj.$prop = $iObj.$prop
                        }
                        $obj
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSUserSchemaField'
function Block-CoreCLREncryptionWarning {
    <#
    .SYNOPSIS
    Blocks CoreCLR encryption warning from reappearing (specific to PSGSuite)
     
    .DESCRIPTION
    Blocks CoreCLR encryption warning from reappearing (specific to PSGSuite) by creating a blank txt file in the path: ~\.scrthq
     
    .EXAMPLE
    Block-CoreCLREncryptionWarning
 
    Creates the breadcrumb file to let PSGSuite know that you've acknowledged the CoreCLR encryption warning and it does not need to be displayed every time the module is imported
    #>

    New-Item -Path (Join-Path (Join-Path "~" ".scrthq") "BlockCoreCLREncryptionWarning.txt") -ItemType File -Force | Out-Null
}
Export-ModuleMember -Function 'Block-CoreCLREncryptionWarning'
function Compare-ModuleVersion {
    <#
    .SYNOPSIS
    Compares the installed version of a module with the latest version on the PowerShell Gallery
 
    .DESCRIPTION
    Compares the installed version of a module with the latest version on the PowerShell Gallery
 
    .PARAMETER ModuleName
    The name of the module to compare
 
    .EXAMPLE
    Compare-ModuleVersion PSGSuite
    #>

    [CmdletBinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [String[]]
        $ModuleName = 'PSGSuite'
    )
    Begin {
        $results = New-Object System.Collections.ArrayList
    }
    Process {
        foreach ($module in $ModuleName) {
            Write-Verbose "Comparing module versions for module '$module'"
            $result = [PSCustomObject][Ordered]@{
                ModuleName       = $module
                InstalledVersion = $null
                GalleryVersion   = $null
                UpdateAvailable  = $null
            }
            if ($InstalledVersion = ((Get-Module -Name $module -ListAvailable).Version | Sort-Object)[-1]) {
                $result.InstalledVersion = $InstalledVersion
                $uri = [Uri]"https://www.powershellgallery.com/api/v2/Packages?`$filter=Id eq '$($module)' and IsLatestVersion"
                if ($GalleryVersion = [System.Version]((Invoke-RestMethod -Uri $uri -Verbose:$false).properties.Version)) {
                    $result.GalleryVersion = $GalleryVersion
                    $result.UpdateAvailable = if ($InstalledVersion -ge $GalleryVersion) {
                        $false
                    }
                    else {
                        $true
                    }
                }
            }
            else {
                Write-Warning "Module '$module' was not found on this machine; unable to compare module versions."
            }
            [void]$results.Add($result)
        }
    }
    End {
        return $results
    }
}

Export-ModuleMember -Function 'Compare-ModuleVersion'
function Unblock-CoreCLREncryptionWarning {
    <#
    .SYNOPSIS
    Unblocks CoreCLR encryption warning to ensure it appears if applicable (specific to PSGSuite)
     
    .DESCRIPTION
    Unblocks CoreCLR encryption warning to ensure it appears if applicable (specific to PSGSuite) by removing the following file if it exists: ~\.scrthq\BlockCoreCLREncryptionWarning.txt
     
    .EXAMPLE
    Unblock-CoreCLREncryptionWarning
 
    Removes the breadcrumb file to let PSGSuite know that you want to receive the CoreCLR encryption warning if applicable
    #>

    if (Test-Path (Join-Path (Join-Path "~" ".scrthq") "BlockCoreCLREncryptionWarning.txt")) {
        Remove-Item -Path (Join-Path (Join-Path "~" ".scrthq") "BlockCoreCLREncryptionWarning.txt") -Force
    }
}
Export-ModuleMember -Function 'Unblock-CoreCLREncryptionWarning'
function Get-GSUserLicense {
    <#
    .SYNOPSIS
    Gets the G Suite license information for a user or list of users
 
    .DESCRIPTION
    Gets the G Suite license information for a user or list of users
 
    .PARAMETER User
    The primary email or unique Id of the user to retrieve license information for
 
    .PARAMETER License
    The license SKU to retrieve information for. If excluded, searches all license SKUs
 
    .PARAMETER ProductId
    The product Id to list licenses for
 
    .PARAMETER PageSize
    The page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .PARAMETER CheckAll
    If $true, force a check of all license products when specifying a User. This will return all license types it finds for a specific user instead of the default behavior of short circuiting after matching against the first license assigned.
 
    .EXAMPLE
    Get-GSUserLicense
 
    Gets the full list of licenses for the customer
    #>

    [OutputType('Google.Apis.Licensing.v1.Data.LicenseAssignment')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "Get")]
        [Alias("PrimaryEmail", "UserKey", "Mail","UserId")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User,
        [parameter(Mandatory = $false, ParameterSetName = "List")]
        [Alias("MaxResults")]
        [ValidateRange(1, 1000)]
        [Int]
        $PageSize = 1000,
        [parameter(Mandatory = $false, ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0,
        [parameter(ParameterSetName = "Get")]
        [Switch]
        $CheckAll
    )
    DynamicParam {
        $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # License
        $_licenses = (Get-LicenseSkuHash).Keys | Sort-Object -Unique
        $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $attribute = New-Object System.Management.Automation.ParameterAttribute
        $attribute.Mandatory = $false
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.AliasAttribute('SkuId')
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.ValidateSetAttribute($_licenses)
        $attributeCollection.Add($attribute)
        $Name = 'License'
        $dynParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Name, [string], $attributeCollection)
        $paramDictionary.Add($Name, $dynParam)

        # ProductId
        $_products = (Get-LicenseProductHash).Keys | Sort-Object -Unique
        $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $attribute = New-Object System.Management.Automation.ParameterAttribute
        $attribute.Mandatory = $false
        $attribute.ParameterSetName = 'List'
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.ValidateSetAttribute($_products)
        $attributeCollection.Add($attribute)
        $Name = 'ProductId'
        $dynParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Name, [string[]], $attributeCollection)
        $paramDictionary.Add($Name, $dynParam)
        # return the collection of dynamic parameters
        return $paramDictionary
    }
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/apps.licensing'
            ServiceType = 'Google.Apis.Licensing.v1.LicensingService'
        }
        $service = New-GoogleService @serviceParams
        $License = $PSBoundParameters['License']
        $ProductId = if ($PSBoundParameters.ContainsKey('ProductId')) {
            $PSBoundParameters['ProductId']
        }
        else {
            (Get-LicenseProductFromDisplayName).Keys | Where-Object {$_ -ne 'Cloud-Identity'} | Sort-Object
        }
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Get {
                    foreach ($U in $User) {
                        $response = $null
                        Resolve-Email ([ref]$U)
                        if ($PSBoundParameters.ContainsKey('License')) {
                            Write-Verbose "Getting License SKU '$License' for User '$U'"
                            $License = Get-LicenseSkuFromDisplayName $License
                            $request = $service.LicenseAssignments.Get((Get-LicenseSkuToProductHash $License), $License, $U)
                            $request.Execute()
                        }
                        else {
                            $matchedLicense = $false
                            foreach ($License in (Get-LicenseSkuFromDisplayName).Keys | Sort-Object) {
                                $response = $null
                                Write-Verbose "Getting License SKU '$License' for User '$U'"
                                $License = Get-LicenseSkuFromDisplayName $License
                                try {
                                    $request = $service.LicenseAssignments.Get((Get-LicenseSkuToProductHash $License), $License, $U)
                                    $response = $request.Execute()
                                }
                                catch {}
                                if (-not $CheckAll -and $response) {
                                    $matchedLicense = $true
                                    break
                                }
                                elseif ($response) {
                                    $matchedLicense = $true
                                    $response
                                }
                            }
                            if (-not $matchedLicense) {
                                Write-Warning "No license found for $U!"
                            }
                        }
                    }
                }
                List {
                    if ($License) {
                        $ProductID = Get-LicenseSkuToProductHash $License
                    }
                    $total = 0
                    $overLimit = $false
                    foreach ($prodId in $ProductID) {
                        $origProdId = $prodId
                        try {
                            if (-not $overLimit) {
                                Write-Verbose "Retrieving licenses for product '$origProdId'"
                                $prodId = Get-LicenseProductHash $prodId
                                if ($License) {
                                    $origLicense = $License
                                    $License = Get-LicenseSkuFromDisplayName $License
                                    $request = $service.LicenseAssignments.ListForProductAndSku($prodId, $License, $Script:PSGSuite.Domain)
                                }
                                else {
                                    $request = $service.LicenseAssignments.ListForProduct($prodId, $Script:PSGSuite.Domain)
                                }
                                if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize, $Limit)
                                    $PageSize = $Limit
                                }
                                $request.MaxResults = $PageSize
                                [int]$i = 1
                                $overLimit = $false
                                do {
                                    $result = $request.Execute()
                                    $result.Items
                                    $total += $result.Items.Count
                                    $request.PageToken = $result.NextPageToken
                                    [int]$retrieved = ($i + $result.Items.Count) - 1
                                    if ($License) {
                                        Write-Verbose "Retrieved $retrieved licenses for product '$origProdId' & sku '$origLicense'..."
                                    }
                                    else {
                                        Write-Verbose "Retrieved $retrieved licenses for product '$origProdId'..."
                                    }
                                    if ($Limit -gt 0 -and $total -eq $Limit) {
                                        Write-Verbose "Limit reached: $Limit"
                                        $overLimit = $true
                                    }
                                    elseif ($Limit -gt 0 -and ($total + $PageSize) -gt $Limit) {
                                        $newPS = $Limit - $total
                                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize, $newPS)
                                        $request.MaxResults = $newPS
                                    }
                                    [int]$i = $i + $result.Items.Count
                                }
                                until ($overLimit -or !$result.NextPageToken)
                            }
                        }
                        catch {
                            if ($_.Exception.Message -notmatch 'Invalid productId') {
                                if ($ErrorActionPreference -eq 'Stop') {
                                    $PSCmdlet.ThrowTerminatingError($_)
                                }
                                else {
                                    Write-Error $_
                                }
                            }
                            else {
                                Write-Verbose "Retrieved $retrieved licenses for product '$origProdId'..."
                            }
                        }
                    }
                    Write-Verbose "Retrieved $total total licenses"
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUserLicense'
function Remove-GSUserLicense {
    <#
    .SYNOPSIS
    Removes a license assignment from a user
 
    .DESCRIPTION
    Removes a license assignment from a user. Useful for restoring a user from a Vault-Former-Employee to an auto-assigned G Suite Business license by removing the Vault-Former-Employee license, for example.
 
    .PARAMETER User
    The user's current primary email address
 
    .PARAMETER License
    The license SKU to remove from the user
 
    .EXAMPLE
    Remove-GSUserLicense -User joe -License Google-Vault-Former-Employee
 
    Removes the Vault-Former-Employee license from Joe
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","UserId")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $User
    )
    DynamicParam {
        $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # License
        $_licenses = (Get-LicenseSkuHash).Keys | Sort-Object -Unique
        $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $attribute = New-Object System.Management.Automation.ParameterAttribute
        $attribute.Mandatory = $true
        $attribute.Position = 1
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.AliasAttribute('SkuId')
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.ValidateSetAttribute($_licenses)
        $attributeCollection.Add($attribute)
        $Name = 'License'
        $dynParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Name, [string], $attributeCollection)
        $paramDictionary.Add($Name, $dynParam)

        return $paramDictionary
    }
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/apps.licensing'
            ServiceType = 'Google.Apis.Licensing.v1.LicensingService'
        }
        $service = New-GoogleService @serviceParams
        $License = $PSBoundParameters['License']
    }
    Process {
        try {
            foreach ($U in $User) {
                Resolve-Email ([ref]$U)
                if ($PSCmdlet.ShouldProcess("Revoking license '$License' from user '$U'")) {
                    Write-Verbose "Revoking license '$License' from user '$U'"
                    $License = Get-LicenseSkuFromDisplayName $License
                    $request = $service.LicenseAssignments.Delete((Get-LicenseSkuToProductHash $License),$License,$U)
                    $request.Execute()
                    Write-Verbose "License revoked for user '$U'"
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSUserLicense'
function Set-GSUserLicense {
    <#
    .SYNOPSIS
    Sets the license for a user
 
    .DESCRIPTION
    Sets the license for a user
 
    .PARAMETER User
    The user's current primary email address
 
    .PARAMETER License
    The license SKU to set for the user
 
    .EXAMPLE
    Set-GSUserLicense -User joe -License Google-Apps-For-Business
 
    Sets Joe to a Google-Apps-For-Business license
    #>

    [OutputType('Google.Apis.Licensing.v1.Data.LicenseAssignment')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","UserId")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User
    )
    DynamicParam {
        $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # License
        $_licenses = (Get-LicenseSkuHash).Keys | Sort-Object -Unique
        $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $attribute = New-Object System.Management.Automation.ParameterAttribute
        $attribute.Mandatory = $true
        $attribute.Position = 1
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.AliasAttribute('SkuId')
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.ValidateSetAttribute($_licenses)
        $attributeCollection.Add($attribute)
        $Name = 'License'
        $dynParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Name, [string], $attributeCollection)
        $paramDictionary.Add($Name, $dynParam)

        return $paramDictionary
    }
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/apps.licensing'
            ServiceType = 'Google.Apis.Licensing.v1.LicensingService'
        }
        $service = New-GoogleService @serviceParams
        $License = $PSBoundParameters['License']
    }
    Process {
        try {
            foreach ($U in $User) {
                Resolve-Email ([ref]$U)
                Write-Verbose "Setting license for $U to $License"
                $License = Get-LicenseSkuFromDisplayName $License
                $body = New-Object 'Google.Apis.Licensing.v1.Data.LicenseAssignmentInsert' -Property @{
                    UserId = $U
                }
                $request = $service.LicenseAssignments.Insert($body,(Get-LicenseSkuToProductHash $License),$License)
                $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Set-GSUserLicense'
function Update-GSUserLicense {
    <#
    .SYNOPSIS
    Reassign a user's product SKU with a different SKU in the same product
 
    .DESCRIPTION
    Reassign a user's product SKU with a different SKU in the same product
 
    .PARAMETER User
    The user's current primary email address
 
    .PARAMETER License
    The license SKU that you would like to reassign the user to
 
    .EXAMPLE
    Update-GSUserLicense -User joe -License G-Suite-Enterprise
 
    Updates Joe to a G-Suite-Enterprise license
    #>

    [OutputType('Google.Apis.Licensing.v1.Data.LicenseAssignment')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","UserId")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User
    )
    DynamicParam {
        $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # License
        $_licenses = (Get-LicenseSkuHash).Keys | Sort-Object -Unique
        $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $attribute = New-Object System.Management.Automation.ParameterAttribute
        $attribute.Mandatory = $false
        $attribute.Position = 1
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.AliasAttribute('SkuId')
        $attributeCollection.Add($attribute)
        $attribute = New-Object System.Management.Automation.ValidateSetAttribute($_licenses)
        $attributeCollection.Add($attribute)
        $Name = 'License'
        $dynParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Name, [string], $attributeCollection)
        $paramDictionary.Add($Name, $dynParam)

        return $paramDictionary
    }
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/apps.licensing'
            ServiceType = 'Google.Apis.Licensing.v1.LicensingService'
        }
        $service = New-GoogleService @serviceParams
        $License = $PSBoundParameters['License']
    }
    Process {
        try {
            foreach ($U in $User) {
                Resolve-Email ([ref]$U)
                Write-Verbose "Setting license for $U to $License"
                $License = Get-LicenseSkuFromDisplayName $License
                $body = Get-GSUserLicense -User $U
                $request = $service.LicenseAssignments.Update($body,(Get-LicenseSkuToProductHash $License),$License,$U)
                $request.Execute()
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSUserLicense'
function Get-GSOrganizationalUnit {
    <#
    .SYNOPSIS
    Gets Organizational Unit information
 
    .DESCRIPTION
    Gets Organizational Unit information
 
    .PARAMETER SearchBase
    The OrgUnitPath you would like to search for. This can be the single OrgUnit to return or the top level of which to return children of
 
    .PARAMETER SearchScope
    The depth at which to return the list of OrgUnits children
 
    Available values are:
    * "Base": only return the OrgUnit specified in the SearchBase
    * "Subtree": return the full list of OrgUnits underneath the specified SearchBase
    * "OneLevel": return the SearchBase and the OrgUnit's directly underneath it
    * "All": same as Subtree
    * "Children": same as OneLevel
 
    Defaults to 'All'
 
    .EXAMPLE
    Get-GSOrganizationalUnit -SearchBase "/" -SearchScope Base
 
    Gets the top level Organizational Unit information
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.OrgUnit')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [Alias('OrgUnitPath','BaseOrgUnitPath')]
        [String]
        $SearchBase,
        [parameter(Mandatory = $false)]
        [Alias('Type')]
        [ValidateSet('Base','Subtree','OneLevel','All','Children')]
        [String]
        $SearchScope = 'All'
    )
    Begin {
        if ($PSBoundParameters.Keys -contains 'SearchBase' -and $SearchBase -ne "/" -and $SearchScope -eq 'Base') {
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/admin.directory.orgunit'
                ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
            }
            $service = New-GoogleService @serviceParams
        }
    }
    Process {
        try {
            if ($PSBoundParameters.Keys -contains 'SearchBase' -and $SearchBase -ne "/" -and $SearchScope -eq 'Base') {
                foreach ($O in $SearchBase) {
                    Write-Verbose "Getting Organizational Unit '$O'"
                    $O = $O.TrimStart('/')
                    $request = $service.Orgunits.Get($Script:PSGSuite.CustomerId,([Google.Apis.Util.Repeatable[String]]::new([String[]]$O)))
                    $request.Execute()
                }
            }
            elseif ($SearchBase -eq "/" -and $SearchScope -eq 'Base') {
                $topId = Get-GSOrganizationalUnitListPrivate -SearchBase "/" -Type Children -Verbose:$false | Where-Object {$_.ParentOrgUnitPath -eq "/"} | Select-Object -ExpandProperty ParentOrgUnitId -Unique
                Get-GSOrganizationalUnit -OrgUnitPath $topId -SearchScope Base
            }
            else {
                Get-GSOrganizationalUnitListPrivate @PSBoundParameters
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSOrganizationalUnit'
function New-GSOrganizationalUnit {
    <#
    .SYNOPSIS
    Creates a new OrgUnit
 
    .DESCRIPTION
    Creates a new Organizational Unit
 
    .PARAMETER Name
    The name of the new OrgUnit
 
    .PARAMETER ParentOrgUnitPath
    The path of the parent OrgUnit
 
    Defaults to "/" (the root OrgUnit)
 
    .PARAMETER ParentOrgUnitId
    The unique ID of the parent organizational unit.
 
    .PARAMETER Description
    Description of the organizational unit.
 
    .PARAMETER BlockInheritance
    Determines if a sub-organizational unit can inherit the settings of the parent organization. The default value is false, meaning a sub-organizational unit inherits the settings of the nearest parent organizational unit. For more information on inheritance and users in an organization structure, see the administration help center: http://support.google.com/a/bin/answer.py?hl=en&answer=182442&topic=1227584&ctx=topic
 
    .EXAMPLE
    New-GSOrganizationalUnit -Name "Test Org" -ParentOrgUnitPath "/Testing" -Description "This is a test OrgUnit"
 
    Creates a new OrgUnit named "Test Org" underneath the existing org unit path "/Testing" with the description "This is a test OrgUnit"
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.OrgUnit')]
    [cmdletbinding(DefaultParameterSetName = 'ParentOrgUnitPath')]
    Param
    (
        [parameter(Mandatory = $true)]
        [String]
        $Name,
        [parameter(Mandatory = $false,ParameterSetName = 'ParentOrgUnitPath')]
        [string]
        $ParentOrgUnitPath,
        [parameter(Mandatory = $false,ParameterSetName = 'ParentOrgUnitId')]
        [string]
        $ParentOrgUnitId,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [Switch]
        $BlockInheritance
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.orgunit'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        if ($PSBoundParameters.Keys -notcontains 'ParentOrgUnitPath' -and $PSCmdlet.ParameterSetName -eq 'ParentOrgUnitPath') {
            $PSBoundParameters['ParentOrgUnitPath'] = '/'
        }
    }
    Process {
        try {
            Write-Verbose "Creating OrgUnit '$Name'"
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.OrgUnit'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            $request = $service.Orgunits.Insert($body,$Script:PSGSuite.CustomerId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSOrganizationalUnit'
function Remove-GSOrganizationalUnit {
    <#
    .SYNOPSIS
    Removes an OrgUnit
     
    .DESCRIPTION
    Removes an Organization Unit
     
    .PARAMETER OrgUnitPath
    The path of the OrgUnit you would like to Remove-GSOrganizationalUnit
     
    .EXAMPLE
    Remove-GSOrganizationalUnit -OrgUnitPath "/Testing"
 
    Removes the OrgUnit "/Testing" on confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String[]]
        $OrgUnitPath
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.orgunit'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($O in $OrgUnitPath) {
                if ($PSCmdlet.ShouldProcess("Deleting OrgUnit at Path '$O'")) {
                    Write-Verbose "Deleting OrgUnit at Path '$O'"
                    $O = $O.TrimStart('/')
                    $request = $service.Orgunits.Delete($Script:PSGSuite.CustomerId,([Google.Apis.Util.Repeatable[String]]::new([String[]]$O)))
                    $request.Execute()
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSOrganizationalUnit'
function Update-GSOrganizationalUnit {
    <#
    .SYNOPSIS
    Updates an OrgUnit
 
    .DESCRIPTION
    Updates an Organizational Unit
 
    .PARAMETER OrgUnitID
    The unique Id of the OrgUnit to update
 
    .PARAMETER OrgUnitPath
    The path of the OrgUnit to update
 
    .PARAMETER Name
    The new name for the OrgUnit
 
    .PARAMETER ParentOrgUnitId
    The new Parent ID for the OrgUnit
 
    .PARAMETER ParentOrgUnitPath
    The path of the new Parent for the OrgUnit
 
    .PARAMETER Description
    The new description for the OrgUnit
 
    .PARAMETER BlockInheritance
    Determines if a sub-organizational unit can inherit the settings of the parent organization. The default value is false, meaning a sub-organizational unit inherits the settings of the nearest parent organizational unit. For more information on inheritance and users in an organization structure, see the administration help center: http://support.google.com/a/bin/answer.py?hl=en&answer=182442&topic=1227584&ctx=topic
 
    .EXAMPLE
    Update-GSOrganizationalUnit -OrgUnitPath "/Testing" -Name "Testing More" -Description "Doing some more testing"
 
    Updates the OrgUnit '/Testing' with a new name "Testing More" and new description "Doing some more testing"
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.OrgUnit')]
    [cmdletbinding(DefaultParameterSetName = "Id")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Id")]
        [ValidateNotNullOrEmpty()]
        [String]
        $OrgUnitID,
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Path")]
        [ValidateNotNullOrEmpty()]
        [String]
        $OrgUnitPath,
        [parameter(Mandatory = $false)]
        [String]
        $Name,
        [parameter(Mandatory = $false)]
        [string]
        $ParentOrgUnitId,
        [parameter(Mandatory = $false)]
        [string]
        $ParentOrgUnitPath,
        [parameter(Mandatory = $false)]
        [String]
        $Description,
        [parameter(Mandatory = $false)]
        [Switch]
        $BlockInheritance
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.orgunit'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            $body = switch ($PSCmdlet.ParameterSetName) {
                Path {
                    Get-GSOrganizationalUnit -SearchBase $OrgUnitPath -SearchScope Base -Verbose:$false
                }
                Id {
                    if ($OrgUnitID -notmatch '^id\:') {
                        $OrgUnitID = "id:$OrgUnitID"
                    }
                    Get-GSOrganizationalUnit -SearchBase $OrgUnitID -SearchScope Base -Verbose:$false
                }
            }
            if ($ParentOrgUnitPath) {
                $body.ParentOrgUnitId = $(Get-GSOrganizationalUnit -OrgUnitPath $ParentOrgUnitPath -Verbose:$false | Select-Object -ExpandProperty OrgUnitID)
            }
            elseif ($ParentOrgUnitId) {
                $body.ParentOrgUnitPath = $(Get-GSOrganizationalUnit -OrgUnitPath $ParentOrgUnitId -Verbose:$false | Select-Object -ExpandProperty OrgUnitPath)
            }
            Write-Verbose "Updating OrgUnit '$OrgUnitPath'"
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_ -and $_ -ne 'OrgUnitPath'}) {
                $body.$prop = $PSBoundParameters[$prop]
            }
            $trimPath = $body.OrgUnitPath.TrimStart('/')
            $request = $service.Orgunits.Patch($body,$Script:PSGSuite.CustomerId,([Google.Apis.Util.Repeatable[String]]::new([String[]]$trimPath)))
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSOrganizationalUnit'
function Get-GSActivityReport {
    <#
    .SYNOPSIS
    Retrieves a list of activities
 
    .DESCRIPTION
    Retrieves a list of activities
 
    .PARAMETER UserKey
    Represents the profile id or the user email for which the data should be filtered. When 'all' is specified as the userKey, it returns usageReports for all users
 
    .PARAMETER ApplicationName
    Application name for which the events are to be retrieved.
 
    Available values are:
    * "Admin": The Admin console application's activity reports return account information about different types of administrator activity events.
    * "Calendar": The G Suite Calendar application's activity reports return information about various Calendar activity events.
    * "Drive": The Google Drive application's activity reports return information about various Google Drive activity events. The Drive activity report is only available for G Suite Business customers.
    * "Groups": The Google Groups application's activity reports return information about various Groups activity events.
    * "GPlus": The Google+ application's activity reports return information about various Google+ activity events.
    * "Login": The G Suite Login application's activity reports return account information about different types of Login activity events.
    * "Mobile": The G Suite Mobile Audit activity report return information about different types of Mobile Audit activity events.
    * "Rules": The G Suite Rules activity report return information about different types of Rules activity events.
    * "Token": The G Suite Token application's activity reports return account information about different types of Token activity events.
 
    Defaults to "Admin"
 
    .PARAMETER EventName
    The name of the event being queried
 
    .PARAMETER ActorIpAddress
    IP Address of host where the event was performed. Supports both IPv4 and IPv6 addresses
 
    .PARAMETER StartTime
    Return events which occurred after this time
 
    .PARAMETER EndTime
    Return events which occurred at or before this time
 
    .PARAMETER Filters
    Event parameters in the form [parameter1 name][operator][parameter1 value]
 
    .PARAMETER PageSize
    Number of activity records to be shown in each page
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSActivityReport -StartTime (Get-Date).AddDays(-30)
 
    Gets the admin activity report for the last 30 days
    #>

    [OutputType('Google.Apis.Admin.Reports.reports_v1.Data.Activity')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserKey = 'all',
        [parameter(Mandatory = $false,Position = 1)]
        [ValidateSet("Admin","Calendar","Drive","Groups","GPlus","Login","Mobile","Rules","Token")]
        [String]
        $ApplicationName = "Admin",
        [parameter(Mandatory = $false,Position = 2)]
        [String]
        $EventName,
        [parameter(Mandatory = $false)]
        [DateTime]
        $StartTime,
        [parameter(Mandatory = $false)]
        [DateTime]
        $EndTime,
        [parameter(Mandatory = $false)]
        [String]
        $ActorIpAddress,
        [parameter(Mandatory = $false)]
        [String[]]
        $Filters,
        [parameter(Mandatory = $false)]
        [ValidateRange(1,1000)]
        [Alias("MaxResults")]
        [Int]
        $PageSize = "1000",
        [parameter(Mandatory = $false)]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.reports.audit.readonly'
            ServiceType = 'Google.Apis.Admin.Reports.reports_v1.ReportsService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            if ($UserKey -notlike "*@*.*" -and $UserKey -cne 'all') {
                $UserKey = "$($UserKey)@$($Script:PSGSuite.Domain)"
            }
            Write-Verbose "Getting $ApplicationName Activity report"
            $request = $service.Activities.List($UserKey,($ApplicationName.ToLower()))
            if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                $PageSize = $Limit
            }
            $request.MaxResults = $PageSize
            foreach ($key in $PSBoundParameters.Keys | Where-Object {$_ -notin @('UserKey','ApplicationName')}) {
                switch ($key) {
                    StartTime {
                        $request.$key = $StartTime.ToString('o')
                    }
                    EndTime {
                        $request.$key = $EndTime.ToString('o')
                    }
                    Filters {
                        $request.$key = $PSBoundParameters[$key] -join ","
                    }
                    Default {
                        if ($request.PSObject.Properties.Name -contains $key) {
                            $request.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            [int]$i = 1
            $overLimit = $false
            do {
                $result = $request.Execute()
                $result.Items
                $request.PageToken = $result.NextPageToken
                [int]$retrieved = ($i + $result.Items.Count) - 1
                Write-Verbose "Retrieved $retrieved events..."
                if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                    Write-Verbose "Limit reached: $Limit"
                    $overLimit = $true
                }
                elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                    $newPS = $Limit - $retrieved
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                    $request.MaxResults = $newPS
                }
                [int]$i = $i + $result.Items.Count
            }
            until ($overLimit -or !$result.NextPageToken)
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSActivityReport'
function Get-GSUsageReport {
    <#
    .SYNOPSIS
    Retrieves the usage report for the specified type.
 
    Defaults to Customer Usage Report type.
 
    .DESCRIPTION
    Retrieves the usage report for the specified type.
 
    Defaults to Customer Usage Report type.
 
    .PARAMETER Date
    Represents the date for which the data is to be fetched. Defaults to 3 days before the current date.
 
    .PARAMETER UserKey
    [User Usage Report] Represents the profile id or the user email for which the data should be filtered.
 
    Use 'all' to retrieve the report for all users.
 
    .PARAMETER EntityType
    [Entity Usage Report] Type of object. Should be one of:
    * gplus_communities
 
    .PARAMETER EntityKey
    [Entity Usage Report] Represents the key of object for which the data should be filtered.
 
    Use 'all' to retrieve the report for all users.
 
    .PARAMETER Filters
    Represents the set of filters including parameter operator value
 
    .PARAMETER Parameters
    Represents the application name, parameter name pairs to fetch in csv as app_name1:param_name1
 
    .PARAMETER PageSize
    Maximum number of results to return. Maximum allowed is 1000
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .PARAMETER Flat
    If $true, returns a flattened object for easy parsing.
 
    .PARAMETER Raw
    If $true, returns the raw, unformatted results.
 
    .EXAMPLE
    Get-GSUsageReport -Date (Get-Date).AddDays(-30)
 
    Gets the Customer Usage report from 30 days prior
    #>

    [cmdletbinding(DefaultParameterSetName = "Customer")]
    Param (
        [parameter(Mandatory = $false,ParameterSetName = "Customer")]
        [parameter(Mandatory = $false,ParameterSetName = "Entity")]
        [parameter(Mandatory = $false,ParameterSetName = "User")]
        [DateTime]
        $Date = (Get-Date).AddDays(-3),
        [parameter(Mandatory = $true,ParameterSetName = "User")]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserKey,
        [parameter(Mandatory = $true,ParameterSetName = "Entity")]
        [ValidateSet('gplus_communities')]
        [String]
        $EntityType,
        [parameter(Mandatory = $false,ParameterSetName = "Entity")]
        [ValidateNotNullOrEmpty()]
        [String]
        $EntityKey = 'all',
        [parameter(Mandatory = $false,ParameterSetName = "Entity")]
        [parameter(Mandatory = $false,ParameterSetName = "User")]
        [String[]]
        $Filters,
        [parameter(Mandatory = $false)]
        [String[]]
        $Parameters,
        [parameter(Mandatory = $false,ParameterSetName = "Entity")]
        [parameter(Mandatory = $false,ParameterSetName = "User")]
        [ValidateRange(1,1000)]
        [Alias("MaxResults")]
        [Int]
        $PageSize = "1000",
        [parameter(Mandatory = $false,ParameterSetName = "Entity")]
        [parameter(Mandatory = $false,ParameterSetName = "User")]
        [Alias('First')]
        [Int]
        $Limit = 0,
        [parameter(Mandatory = $false)]
        [switch]
        $Flat,
        [parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.reports.usage.readonly'
            ServiceType = 'Google.Apis.Admin.Reports.reports_v1.ReportsService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Getting $($PSCmdlet.ParameterSetName) Usage report for $($Date.ToString('yyyy-MM-dd'))"
            switch ($PSCmdlet.ParameterSetName) {
                Customer {
                    $request = $service.CustomerUsageReports.Get(($Date.ToString('yyyy-MM-dd')))
                }
                Entity {
                    $request = $service.EntityUsageReports.Get($EntityType,$EntityKey,($Date.ToString('yyyy-MM-dd')))
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.MaxResults = $PageSize
                }
                User {
                    if ($UserKey -ceq 'me') {
                        $UserKey = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($UserKey -notlike "*@*.*" -and $UserKey -ne 'all') {
                        $UserKey = "$($UserKey)@$($Script:PSGSuite.Domain)"
                    }
                    $request = $service.UserUsageReport.Get($UserKey,($Date.ToString('yyyy-MM-dd')))
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.MaxResults = $PageSize
                }
            }
            foreach ($key in $PSBoundParameters.Keys | Where-Object {$_ -notin @('Date','UserKey','EntityKey','EntityType')}) {
                switch ($key) {
                    Filters {
                        $request.$key = $PSBoundParameters[$key] -join ","
                    }
                    Parameters {
                        $request.$key = $PSBoundParameters[$key] -join ","
                    }
                    Default {
                        if ($request.PSObject.Properties.Name -contains $key) {
                            $request.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            $warnings = @()
            [int]$i = 1
            $overLimit = $false
            do {
                $result = $request.Execute()
                if ($Raw) {
                    $result.UsageReportsValue
                }
                else {
                    $result.UsageReportsValue | ForEach-Object {
                        if ($null -ne $_) {
                            $orig = $_
                            $orig | Add-Member -MemberType NoteProperty -Name CustomerId -Value $orig.Entity.CustomerId -Force -PassThru | Add-Member -MemberType NoteProperty -Name EntityType -Value $orig.Entity.Type -Force
                            switch ($PSCmdlet.ParameterSetName) {
                                Entity {
                                    $orig | Add-Member -MemberType NoteProperty -Name EntityKey -Value $orig.Entity.EntityKey -Force
                                    $orig | Add-Member -MemberType NoteProperty -Name CommunityName -Value $orig.Parameters[$orig.Parameters.Name.IndexOf('gplus:community_name')].StringValue -Force
                                }
                                User {
                                    $orig | Add-Member -MemberType NoteProperty -Name Email -Value $orig.Entity.UserEmail -Force -PassThru | Add-Member -MemberType NoteProperty -Name UserEmail -Value $orig.Entity.UserEmail -Force -PassThru | Add-Member -MemberType NoteProperty -Name ProfileId -Value $orig.Entity.ProfileId -Force
                                }
                            }
                            foreach ($param in $orig.Parameters | Sort-Object Name) {
                                if ($null -ne $param.Name) {
                                    $paramValue = if ($null -ne $param.StringValue) {
                                        $param.StringValue
                                    }
                                    elseif ($null -ne $param.IntValue) {
                                        $param.IntValue
                                    }
                                    elseif ($null -ne $param.DatetimeValue) {
                                        $param.DatetimeValue
                                    }
                                    elseif ($null -ne $param.BoolValue) {
                                        $param.BoolValue
                                    }
                                    elseif ($null -ne $param.MsgValue) {
                                        $param.MsgValue
                                    }
                                    else {
                                        $null
                                    }
                                    if ($Flat) {
                                        $orig | Add-Member -MemberType NoteProperty -Name $param.Name -Value $paramValue -Force
                                    }
                                    else {
                                        $pName = $param.Name -split ":"
                                        if ($orig.PSObject.Properties.Name -notcontains $pName[0]) {
                                            $orig | Add-Member -MemberType NoteProperty -Name $pName[0] -Value $([Ordered]@{}) -Force
                                        }
                                        $orig.$($pName[0])[$pName[1]] = $paramValue
                                    }
                                }
                            }
                            $orig
                        }
                    }
                }
                $warnings += $result.Warnings
                $request.PageToken = $result.NextPageToken
                [int]$retrieved = ($i + $result.UsageReportsValue.Count) - 1
                Write-Verbose "Retrieved $retrieved entities for this report..."
                if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                    Write-Verbose "Limit reached: $Limit"
                    $overLimit = $true
                }
                elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                    $newPS = $Limit - $retrieved
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                    $request.MaxResults = $newPS
                }
                [int]$i = $i + $result.UsageReportsValue.Count
            }
            until ($overLimit -or !$result.NextPageToken)
            if ($warnings | Where-Object {$_.Code}) {
                $warnings | ForEach-Object {
                    Write-Warning "[$($_.Code)] $($_.Message)"
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUsageReport'
function Get-GSResource {
    <#
    .SYNOPSIS
    Gets Calendar Resources (Calendars, Buildings & Features supported)
 
    .DESCRIPTION
    Gets Calendar Resources (Calendars, Buildings & Features supported)
 
    .PARAMETER Id
    If Id is provided, gets the Resource by Id
 
    .PARAMETER Resource
    The Resource Type to List
 
    Available values are:
    * "Calendars": resource calendars (legacy and new - i.e. conference rooms)
    * "Buildings": new Building Resources (i.e. "Building A" or "North Campus")
    * "Features": new Feature Resources (i.e. "Video Conferencing" or "Projector")
 
    .PARAMETER Filter
    String query used to filter results. Should be of the form "field operator value" where field can be any of supported fields and operators can be any of supported operations. Operators include '=' for exact match and ':' for prefix match or HAS match where applicable. For prefix match, the value should always be followed by a *. Supported fields include generatedResourceName, name, buildingId, featureInstances.feature.name. For example buildingId=US-NYC-9TH AND featureInstances.feature.name:Phone.
 
    PowerShell filter syntax here is supported as "best effort". Please use Google's filter operators and syntax to ensure best results
 
    .PARAMETER OrderBy
    Field(s) to sort results by in either ascending or descending order. Supported fields include resourceId, resourceName, capacity, buildingId, and floorName.
 
    .PARAMETER PageSize
    Page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSResource -Resource Buildings
 
    Gets the full list of Buildings Resources
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Get")]
        [String[]]
        $Id,
        [parameter(Mandatory = $false,Position = 1)]
        [ValidateSet('Calendars','Buildings','Features')]
        [String]
        $Resource = 'Calendars',
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('Query')]
        [String[]]
        $Filter,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [String[]]
        $OrderBy,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateRange(1,500)]
        [Alias("MaxResults")]
        [Int]
        $PageSize = 500,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Begin {
        if ($MyInvocation.InvocationName -eq 'Get-GSCalendarResourceList') {
            $Resource = 'Calendars'
        }
        $propHash = @{
            Calendars = 'Items'
            Buildings = 'BuildingsValue'
            Features  = 'FeaturesValue'
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.resource.calendar'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            'my_customer'
        }
    }
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($I in $Id) {
                    try {
                        Write-Verbose "Getting Resource $Resource Id '$I'"
                        $request = $service.Resources.$Resource.Get($customerId,$I)
                        $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Id' -Value $I -PassThru | Add-Member -MemberType NoteProperty -Name 'Resource' -Value $Resource -PassThru | Add-Member -MemberType ScriptMethod -Name ToString -Value { $this.Id } -PassThru -Force
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            List {
                try {
                    $request = $service.Resources.$Resource.List($customerId)
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.MaxResults = $PageSize
                    if ($Resource -eq 'Calendars') {
                        if ($OrderBy) {
                            $request.OrderBy = "$($OrderBy -join ", ")"
                        }
                        if ($Filter) {
                            if ($Filter -eq '*') {
                                $Filter = ""
                            }
                            else {
                                $Filter = "$($Filter -join " ")"
                            }
                            $Filter = $Filter -replace " -eq ","=" -replace " -like ",":" -replace " -match ",":" -replace " -contains ",":" -creplace "'True'","True" -creplace "'False'","False"
                            $request.Query = $Filter.Trim()
                            Write-Verbose "Getting Resource $Resource matching filter: `"$($Filter.Trim())`""
                        }
                        else {
                            Write-Verbose "Getting all Resource $Resource"
                        }
                    }
                    else {
                        Write-Verbose "Getting all Resource $Resource"
                    }
                    [int]$i = 1
                    $overLimit = $false
                    do {
                        $result = $request.Execute()
                        $result.$($propHash[$Resource]) | ForEach-Object {
                            $obj = $_
                            $_Id = switch ($Resource) {
                                Calendars {
                                    $obj.ResourceId
                                }
                                Buildings {
                                    $obj.BuildingId
                                }
                                Features {
                                    $obj.Name
                                }
                            }
                            $_ | Add-Member -MemberType NoteProperty -Name 'Id' -Value $_Id -PassThru | Add-Member -MemberType NoteProperty -Name 'Resource' -Value $Resource -PassThru | Add-Member -MemberType ScriptMethod -Name ToString -Value { $this.Id } -PassThru -Force
                        }
                        if ($result.NextPageToken) {
                            $request.PageToken = $result.NextPageToken
                        }
                        [int]$retrieved = ($i + $result.$($propHash[$Resource]).Count) - 1
                        Write-Verbose "Retrieved $retrieved resources..."
                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                            Write-Verbose "Limit reached: $Limit"
                            $overLimit = $true
                        }
                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                            $newPS = $Limit - $retrieved
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                            $request.MaxResults = $newPS
                        }
                        [int]$i = $i + $result.$($propHash[$Resource]).Count
                    }
                    until ($overLimit -or !$result.NextPageToken)
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSResource'
function New-GSResource {
    <#
    .SYNOPSIS
    Creates a new Calendar Resource
     
    .DESCRIPTION
    Creates a new Calendar Resource. Supports Resource types 'Calendars','Buildings' & 'Features'
     
    .PARAMETER Name
    The name of the new Resource
     
    .PARAMETER Id
    The unique ID for the calendar resource.
     
    .PARAMETER BuildingId
    Unique ID for the building a resource is located in.
     
    .PARAMETER Description
    Description of the resource, visible only to admins.
     
    .PARAMETER Capacity
    Capacity of a resource, number of seats in a room.
     
    .PARAMETER FloorName
    Name of the floor a resource is located on (Calendars Resource type)
     
    .PARAMETER FloorNames
    The names of the floors in the building (Buildings Resource type)
     
    .PARAMETER FloorSection
    Name of the section within a floor a resource is located in.
     
    .PARAMETER Category
    The category of the calendar resource. Either CONFERENCE_ROOM or OTHER. Legacy data is set to CATEGORY_UNKNOWN.
 
    Acceptable values are:
    * "CATEGORY_UNKNOWN"
    * "CONFERENCE_ROOM"
    * "OTHER"
 
    Defaults to 'CATEGORY_UNKNOWN' if creating a Calendar Resource
     
    .PARAMETER ResourceType
    The type of the calendar resource, intended for non-room resources.
     
    .PARAMETER UserVisibleDescription
    Description of the resource, visible to users and admins.
     
    .PARAMETER Resource
    The resource type you would like to create
 
    Available values are:
    * "Calendars": create a Resource Calendar or legacy resource type
    * "Buildings": create a Resource Building
    * "Features": create a Resource Feature
     
    .EXAMPLE
    New-GSResource -Name "Training Room" -Id "Train01" -Capacity 75 -Category 'CONFERENCE_ROOM' -ResourceType "Conference Room" -Description "Training room for new hires - has 1 LAN port per station" -UserVisibleDescription "Training room for new hires"
 
    Creates a new training room Resource Calendar that will be bookable on Google Calendar
    #>

    [CmdletBinding(DefaultParameterSetName = 'Features')]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $Name,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [Alias('ResourceId')]
        [String]
        $Id,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [parameter(Mandatory = $false,ParameterSetName = 'Buildings')]
        [String]
        $BuildingId,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [parameter(Mandatory = $false,ParameterSetName = 'Buildings')]
        [String]
        $Description,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [Int]
        $Capacity,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $FloorName,
        [parameter(Mandatory = $false,ParameterSetName = 'Buildings')]
        [String[]]
        $FloorNames,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $FloorSection,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [ValidateSet('CATEGORY_UNKNOWN','CONFERENCE_ROOM','OTHER')]
        [String]
        $Category,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $ResourceType,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $UserVisibleDescription,
        [parameter(Mandatory = $false)]
        [ValidateSet('Calendars','Buildings','Features')]
        [String]
        $Resource
    )
    Begin {
        $resType = if ($MyInvocation.InvocationName -eq 'New-GSCalendarResource') {
            'Calendars'
        }
        elseif ($Resource) {
            $Resource
        }
        else {
            $PSCmdlet.ParameterSetName
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.resource.calendar'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        if ($PSBoundParameters -notcontains 'Category' -and $PSCmdlet.ParameterSetName -eq 'Calendars') {
            $PSBoundParameters['Category'] = 'CATEGORY_UNKNOWN'
        }
    }
    Process {
        try {
            Write-Verbose "Creating Resource $resType '$Name'"
            $body = New-Object "$(switch ($resType) {
                Calendars {
                    'Google.Apis.Admin.Directory.directory_v1.Data.CalendarResource'
                }
                Buildings {
                    'Google.Apis.Admin.Directory.directory_v1.Data.Building'
                }
                Features {
                    'Google.Apis.Admin.Directory.directory_v1.Data.Feature'
                }
            })"

            foreach ($key in $PSBoundParameters.Keys) {
                switch ($key) {
                    Category {
                        $body.ResourceCategory = $PSBoundParameters[$key]
                    }
                    Id {
                        if ($resType -eq 'Calendars') {
                            $body.ResourceId = $PSBoundParameters[$key]
                        }
                        else {
                            $body.$key = $PSBoundParameters[$key]
                        }
                        
                    }
                    Name {
                        if ($resType -eq 'Calendars') {
                            $body.ResourceName = $PSBoundParameters[$key]
                        }
                        elseif ($resType -eq 'Buildings') {
                            $body.BuildingName = $PSBoundParameters[$key]
                        }
                        else {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                    Description {
                        if ($resType -eq 'Calendars') {
                            $body.ResourceDescription = $PSBoundParameters[$key]
                        }
                        else {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                    Default {
                        if ($body.PSObject.Properties.Name -contains $key) {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            $request = $service.Resources.$resType.Insert($body,$(if ($Script:PSGSuite.CustomerID) {
                        $Script:PSGSuite.CustomerID
                    }
                    else {
                        'my_customer'
                    }))
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Resource' -Value $resType -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}
Export-ModuleMember -Function 'New-GSResource'
function Remove-GSResource {
    <#
    .SYNOPSIS
    Removes a resource
     
    .DESCRIPTION
    Removes a resource
     
    .PARAMETER ResourceId
    The Resource Id of the Resource *Calendar* you would like to remove
     
    .PARAMETER BuildingId
    The Building Id of the Resource *Building* you would like to remove
     
    .PARAMETER FeatureKey
    The Feature Key of the Resource *Feature* you would like to remove
     
    .EXAMPLE
    Remove-GSResource -ResourceId Train01
 
    Removes the Resource Calendar 'Train01' after confirmation
    #>

    [CmdletBinding(SupportsShouldProcess = $true,ConfirmImpact = "High",DefaultParameterSetName = 'Calendars')]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true,ParameterSetName = 'Calendars')]
        [Alias('CalendarResourceId')]
        [String[]]
        $ResourceId,
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true,ParameterSetName = 'Buildings')]
        [String[]]
        $BuildingId,
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true,ParameterSetName = 'Features')]
        [Alias('Name')]
        [String[]]
        $FeatureKey
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.resource.calendar'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Calendars {
                    $Resource = 'Calendars'
                    $list = $ResourceId
                }
                Buildings {
                    $Resource = 'Buildings'
                    $list = $BuildingId
                }
                Features {
                    $Resource = 'Features'
                    $list = $FeatureKey
                }
            }
            foreach ($I in $list) {
                if ($PSCmdlet.ShouldProcess("Deleting Resource $Resource Id '$I'")) {
                    Write-Verbose "Deleting Resource $Resource Id '$I'"
                    $request = $service.Resources.$Resource.Delete($(if($Script:PSGSuite.CustomerID){$Script:PSGSuite.CustomerID}else{'my_customer'}),$I)
                    $request.Execute()
                    Write-Verbose "Resource $Resource Id '$I' has been successfully deleted"
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSResource'
function Update-GSResource {
    <#
    .SYNOPSIS
    Updates a Calendar Resource
 
    .DESCRIPTION
    Updates a Calendar Resource
 
    .PARAMETER ResourceId
    The unique Id of the Resource Calendar that you would like to update
 
    .PARAMETER BuildingId
    If updating a Resource Building, the unique Id of the building you would like to update
 
    If updating a Resource Calendar, the new Building Id for the resource
 
    .PARAMETER FeatureKey
    The unique key of the Feature you would like to update
 
    .PARAMETER Name
    The new name of the resource
 
    .PARAMETER Description
    Description of the resource, visible only to admins.
 
    .PARAMETER Capacity
    Capacity of a resource, number of seats in a room.
 
    .PARAMETER FloorName
    Name of the floor a resource is located on (Calendars Resource type)
 
    .PARAMETER FloorNames
    The names of the floors in the building (Buildings Resource type)
 
    .PARAMETER FloorSection
    Name of the section within a floor a resource is located in.
 
    .PARAMETER Category
    The new category of the calendar resource. Either CONFERENCE_ROOM or OTHER. Legacy data is set to CATEGORY_UNKNOWN.
 
    Acceptable values are:
    * "CATEGORY_UNKNOWN"
    * "CONFERENCE_ROOM"
    * "OTHER"
 
    Defaults to 'CATEGORY_UNKNOWN' if creating a Calendar Resource
 
    .PARAMETER ResourceType
    The type of the calendar resource, intended for non-room resources.
 
    .PARAMETER UserVisibleDescription
    Description of the resource, visible to users and admins.
 
    .PARAMETER Resource
    The resource type you would like to update
 
    Available values are:
    * "Calendars": create a Resource Calendar or legacy resource type
    * "Buildings": create a Resource Building
    * "Features": create a Resource Feature
 
    .EXAMPLE
    Update-GSResource -ResourceId Train01 -Name 'Corp Training Room'
 
    Updates the calendar resource with ID 'Train01' to the new name 'Corp Training Room'
    #>

    [CmdletBinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true,ParameterSetName = 'Calendars')]
        [Alias('CalendarResourceId','Id')]
        [String]
        $ResourceId,
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true,ParameterSetName = 'Buildings')]
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $BuildingId,
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true,ParameterSetName = 'Features')]
        [String]
        $FeatureKey,
        [parameter(Mandatory = $false,Position = 0)]
        [String]
        $Name,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [parameter(Mandatory = $false,ParameterSetName = 'Buildings')]
        [String]
        $Description,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [Int]
        $Capacity,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $FloorName,
        [parameter(Mandatory = $false,ParameterSetName = 'Buildings')]
        [String[]]
        $FloorNames,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $FloorSection,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [ValidateSet('CATEGORY_UNKNOWN','CONFERENCE_ROOM','OTHER')]
        [String]
        $Category,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $ResourceType,
        [parameter(Mandatory = $false,ParameterSetName = 'Calendars')]
        [String]
        $UserVisibleDescription,
        [parameter(Mandatory = $false)]
        [ValidateSet('Calendars','Buildings','Features')]
        [String]
        $Resource
    )
    Process {
        $resType = if ($MyInvocation.InvocationName -eq 'Update-GSCalendarResource') {
            'Calendars'
        }
        elseif ($Resource) {
            $Resource
        }
        else {
            $PSCmdlet.ParameterSetName
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.resource.calendar'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        if ($PSBoundParameters -notcontains 'Category' -and $PSCmdlet.ParameterSetName -eq 'Calendars') {
            $PSBoundParameters['Category'] = 'CATEGORY_UNKNOWN'
        }
        try {
            Write-Verbose "Creating Resource $resType '$Name'"
            $body = New-Object "$(switch ($resType) {
                Calendars {
                    'Google.Apis.Admin.Directory.directory_v1.Data.CalendarResource'
                }
                Buildings {
                    'Google.Apis.Admin.Directory.directory_v1.Data.Building'
                }
                Features {
                    'Google.Apis.Admin.Directory.directory_v1.Data.Feature'
                }
            })"

            foreach ($key in $PSBoundParameters.Keys) {
                switch ($key) {
                    Category {
                        $body.ResourceCategory = $PSBoundParameters[$key]
                    }
                    Name {
                        if ($resType -eq 'Calendars') {
                            $body.ResourceName = $PSBoundParameters[$key]
                        }
                        elseif ($resType -eq 'Buildings') {
                            $body.BuildingName = $PSBoundParameters[$key]
                        }
                        else {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                    Description {
                        if ($resType -eq 'Calendars') {
                            $body.ResourceDescription = $PSBoundParameters[$key]
                        }
                        else {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                    Default {
                        if ($body.PSObject.Properties.Name -contains $key) {
                            $body.$key = $PSBoundParameters[$key]
                        }
                    }
                }
            }
            $resId = switch ($PSCmdlet.ParameterSetName) {
                Calendars {
                    $ResourceId
                }
                Buildings {
                    $BuildingId
                }
                Features {
                    $FeatureKey
                }
            }
            $request = $service.Resources.$resType.Patch($body,$(if ($Script:PSGSuite.CustomerID) {
                        $Script:PSGSuite.CustomerID
                    }
                    else {
                        'my_customer'
                    }),$resId)
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'Resource' -Value $resType -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSResource'
function Get-GSAdminRoleAssignment {
    <#
    .SYNOPSIS
    Gets a specific Admin Role Assignments or the list of Admin Role Assignments for a given role
 
    .DESCRIPTION
    Gets a specific Admin Role Assignments or the list of Admin Role Assignments for a given role
 
    .PARAMETER RoleAssignmentId
    The RoleAssignmentId(s) you would like to retrieve info for.
 
    If left blank, returns the full list of Role Assignments
 
    .PARAMETER UserKey
    The UserKey(s) you would like to retrieve Role Assignments for. This can be a user's email or their unique UserId
 
    If left blank, returns the full list of Role Assignments
 
    .PARAMETER RoleId
    The RoleId(s) you would like to retrieve Role Assignments for.
 
    If left blank, returns the full list of Role Assignments
 
    .PARAMETER PageSize
    Page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSAdminRoleAssignment
 
    Gets the list of Admin Role Assignments
 
    .EXAMPLE
    Get-GSAdminRoleAssignment -RoleId 9191482342768644,9191482342768642
 
    Gets the Admin Role Assignments matching the provided RoleIds
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.RoleAssignment')]
    [cmdletbinding(DefaultParameterSetName = "ListUserKey")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ParameterSetName = "Get")]
        [String[]]
        $RoleAssignmentId,
        [parameter(Mandatory = $false,ParameterSetName = "ListUserKey")]
        [string[]]
        $UserKey,
        [parameter(Mandatory = $false,ParameterSetName = "ListRoleId")]
        [string[]]
        $RoleId,
        [parameter(Mandatory = $false,ParameterSetName = "ListUserKey")]
        [parameter(Mandatory = $false,ParameterSetName = "ListRoleId")]
        [ValidateRange(1,100)]
        [Alias("MaxResults")]
        [Int]
        $PageSize = 100,
        [parameter(Mandatory = $false,ParameterSetName = "ListUserKey")]
        [parameter(Mandatory = $false,ParameterSetName = "ListRoleId")]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            'my_customer'
        }
    }
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($Role in $RoleAssignmentId) {
                    try {
                        Write-Verbose "Getting Admin Role Assignment '$Role'"
                        $request = $service.RoleAssignments.Get($customerId,$Role)
                        $request.Execute()
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            Default {
                [int]$i = 1
                $overLimit = $false
                Write-Verbose "Getting Admin Role Assignment List"
                $baseRequest = $service.RoleAssignments.List($customerId)
                if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                    $PageSize = $Limit
                }
                $baseRequest.MaxResults = $PageSize
                if ($PSBoundParameters.Keys -contains 'RoleId' -or $PSBoundParameters.Keys -contains 'UserKey') {
                    switch ($PSBoundParameters.Keys) {
                        RoleId {
                            foreach ($Role in $RoleId) {
                                if (-not $overLimit) {
                                    try {
                                        $request = $baseRequest
                                        $request.RoleId = $Role
                                        do {
                                            $result = $request.Execute()
                                            $result.Items | Add-Member -MemberType NoteProperty -Name 'Filter' -Value ([PSCustomObject]@{RoleId = $Role}) -PassThru
                                            $request.PageToken = $result.NextPageToken
                                            [int]$retrieved = ($i + $result.Items.Count) - 1
                                            Write-Verbose "Retrieved $retrieved role assignments..."
                                            if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                                                Write-Verbose "Limit reached: $Limit"
                                                $overLimit = $true
                                            }
                                            elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                                                $newPS = $Limit - $retrieved
                                                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                                                $request.MaxResults = $newPS
                                            }
                                            [int]$i = $i + $result.Items.Count
                                        }
                                        until ($overLimit -or !$result.NextPageToken)
                                    }
                                    catch {
                                        if ($ErrorActionPreference -eq 'Stop') {
                                            $PSCmdlet.ThrowTerminatingError($_)
                                        }
                                        else {
                                            Write-Error $_
                                        }
                                    }
                                }
                            }
                        }
                        UserKey {
                            foreach ($User in $UserKey) {
                                try {
                                    $request = $baseRequest
                                    $uKey = try {
                                        [int64]$User
                                    }
                                    catch {
                                        if ($User -ceq 'me') {
                                            $User = $Script:PSGSuite.AdminEmail
                                        }
                                        elseif ($User -notlike "*@*.*") {
                                            $User = "$($User)@$($Script:PSGSuite.Domain)"
                                        }
                                        (Get-GSUser -User $User -Verbose:$false).Id
                                    }
                                    $request.UserKey = $uKey
                                    do {
                                        $result = $request.Execute()
                                        $result.Items | Add-Member -MemberType NoteProperty -Name 'Filter' -Value ([PSCustomObject]@{UserKey = $User}) -PassThru
                                        $request.PageToken = $result.NextPageToken
                                        [int]$retrieved = ($i + $result.Items.Count) - 1
                                        Write-Verbose "Retrieved $retrieved role assignments..."
                                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                                            Write-Verbose "Limit reached: $Limit"
                                            $overLimit = $true
                                        }
                                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                                            $newPS = $Limit - $retrieved
                                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                                            $request.MaxResults = $newPS
                                        }
                                        [int]$i = $i + $result.Items.Count
                                    }
                                    until ($overLimit -or !$result.NextPageToken)
                                }
                                catch {
                                    if ($ErrorActionPreference -eq 'Stop') {
                                        $PSCmdlet.ThrowTerminatingError($_)
                                    }
                                    else {
                                        Write-Error $_
                                    }
                                }
                            }
                        }
                    }
                }
                else {
                    try {
                        $request = $baseRequest
                        do {
                            $result = $request.Execute()
                            $result.Items
                            $request.PageToken = $result.NextPageToken
                            [int]$retrieved = ($i + $result.Items.Count) - 1
                            Write-Verbose "Retrieved $retrieved role assignments..."
                            if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                                Write-Verbose "Limit reached: $Limit"
                                $overLimit = $true
                            }
                            elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                                $newPS = $Limit - $retrieved
                                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                                $request.MaxResults = $newPS
                            }
                            [int]$i = $i + $result.Items.Count
                        }
                        until ($overLimit -or !$result.NextPageToken)
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSAdminRoleAssignment'
function New-GSAdminRoleAssignment {
    <#
    .SYNOPSIS
    Creates a new Admin Role Assignment
 
    .DESCRIPTION
    Creates a new Admin Role Assignment
 
    .PARAMETER AssignedTo
    The unique ID of the user this role is assigned to.
 
    .PARAMETER RoleId
    The ID of the role that is assigned.
 
    .PARAMETER OrgUnitId
    If the role is restricted to an organization unit, this contains the ID for the organization unit the exercise of this role is restricted to.
 
    .PARAMETER ScopeType
    The scope in which this role is assigned.
 
    Acceptable values are:
    * "CUSTOMER"
    * "ORG_UNIT"
 
    .EXAMPLE
    New-GSAdminRoleAssignment -AssignedTo jsmith -RoleId 9191482342768644
 
    Assign a new role to a given user.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.RoleAssignment')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true, Position = 0)]
        [String[]]
        $AssignedTo,
        [parameter(Mandatory = $true)]
        [Int64]
        $RoleId,
        [parameter(Mandatory = $false)]
        [String]
        $OrgUnitId,
        [parameter(Mandatory = $false)]
        [ValidateSet('CUSTOMER', 'ORG_UNIT')]
        [String]
        $ScopeType = 'CUSTOMER'
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.rolemanagement'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams

        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            'my_customer'
        }
    }
    Process {
        foreach ($Assigned in $AssignedTo) {
            try {
                $uKey = try {
                    [int64]$Assigned
                }
                catch {
                    if ($Assigned -ceq 'me') {
                        $Assigned = $Script:PSGSuite.AdminEmail
                    }
                    elseif ($Assigned -notlike "*@*.*") {
                        $Assigned = "$($Assigned)@$($Script:PSGSuite.Domain)"
                    }
                    (Get-GSUser -User $Assigned -Verbose:$false).Id
                }
                $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.RoleAssignment'
                $body.ScopeType = $ScopeType
                foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                    switch ($prop) {
                        AssignedTo {
                            $body.AssignedTo = $uKey
                        }
                        Default {
                            $body.$prop = $PSBoundParameters[$prop]
                        }
                    }
                }
                Write-Verbose "Creating Admin Role Assignment for user '$Assigned' for Role Id '$RoleId'"
                $request = $service.RoleAssignments.Insert($body, $customerId)
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSAdminRoleAssignment'
function Remove-GSAdminRoleAssignment {
    <#
    .SYNOPSIS
    Removes a specific Admin Role Assignment or the list of Admin Role Assignments
     
    .DESCRIPTION
    Removes a specific Admin Role Assignment or the list of Admin Role Assignments
     
    .PARAMETER RoleAssignmentId
    The RoleAssignmentId(s) you would like to remove
     
    .EXAMPLE
    Remove-GSAdminRoleAssignment -RoleAssignmentId 9191482342768644,9191482342768642
 
    Removes the role assignments matching the provided Ids
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [String[]]
        $RoleAssignmentId
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.rolemanagement'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            'my_customer'
        }
    }
    Process {
        foreach ($Role in $RoleAssignmentId) {
            try {
                if ($PSCmdlet.ShouldProcess("Deleting Role Assignment Id '$Role'")) {
                    Write-Verbose "Deleting Role Assignment Id '$Role'"
                    $request = $service.RoleAssignments.Delete($customerId,$Role)
                    $request.Execute()
                    Write-Verbose "Role Assignment Id '$Role' has been successfully deleted"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSAdminRoleAssignment'
function Get-GSAdminRole {
    <#
    .SYNOPSIS
    Gets a specific Admin Role or the list of Admin Roles
 
    .DESCRIPTION
    Gets a specific Admin Role or the list of Admin Roles
 
    .PARAMETER RoleId
    The RoleId(s) you would like to retrieve info for.
 
    If left blank, returns the full list of Roles
 
    .PARAMETER PageSize
    Page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .EXAMPLE
    Get-GSAdminRole
 
    Gets the list of Admin Roles
 
    .EXAMPLE
    Get-GSAdminRole -RoleId 9191482342768644,9191482342768642
 
    Gets the admin roles matching the provided Ids
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Role')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ParameterSetName = "Get")]
        [int64[]]
        $RoleId,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateRange(1,100)]
        [Alias("MaxResults")]
        [Int]
        $PageSize = 100,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.rolemanagement'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            'my_customer'
        }
    }
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            Get {
                foreach ($Role in $RoleId) {
                    try {
                        Write-Verbose "Getting Admin Role '$Role'"
                        $request = $service.Roles.Get($customerId,$Role)
                        $request.Execute()
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
            }
            List {
                try {
                    Write-Verbose "Getting Admin Role List"
                    $request = $service.Roles.List($customerId)
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.MaxResults = $PageSize
                    [int]$i = 1
                    $overLimit = $false
                    do {
                        $result = $request.Execute()
                        $result.Items
                        $request.PageToken = $result.NextPageToken
                        [int]$retrieved = ($i + $result.Items.Count) - 1
                        Write-Verbose "Retrieved $retrieved roles..."
                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                            Write-Verbose "Limit reached: $Limit"
                            $overLimit = $true
                        }
                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                            $newPS = $Limit - $retrieved
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                            $request.MaxResults = $newPS
                        }
                        [int]$i = $i + $result.Items.Count
                    }
                    until ($overLimit -or !$result.NextPageToken)
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSAdminRole'
function New-GSAdminRole {
    <#
    .SYNOPSIS
    Creates a new Admin Role
 
    .DESCRIPTION
    Creates a new Admin Role
 
    .PARAMETER RoleName
    The name of the new role
 
    .PARAMETER RolePrivileges
    The set of privileges that are granted to this role.
 
    .PARAMETER RoleDescription
    A short description of the role.
 
    .EXAMPLE
    Get-GSAdminRole
 
    Gets the list of Admin Roles
 
    .EXAMPLE
    Get-GSAdminRole -RoleId '9191482342768644','9191482342768642'
 
    Gets the admin roles matching the provided Ids
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Role')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $RoleName,
        [parameter(Mandatory = $true,Position = 1,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Google.Apis.Admin.Directory.directory_v1.Data.Role+RolePrivilegesData[]]
        $RolePrivileges,
        [parameter(Mandatory = $false)]
        [String]
        $RoleDescription
    )
    Begin {
        if ($PSCmdlet.ParameterSetName -eq 'Get') {
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/admin.directory.rolemanagement'
                ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
            }
            $service = New-GoogleService @serviceParams
        }
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            'my_customer'
        }
        $privArray = New-Object 'System.Collections.Generic.List[Google.Apis.Admin.Directory.directory_v1.Data.Role+RolePrivilegesData]'
    }
    Process {
        foreach ($Privilege in $RolePrivileges) {
            $privArray.Add($Privilege)
        }
    }
    End {
        try {
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Role'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    RolePrivileges {
                        $body.RolePrivileges = $privArray
                    }
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            Write-Verbose "Creating Admin Role '$RoleName' with the following privileges:`n`t- $($privArray.PrivilegeName -join "`n`t- ")"
            $request = $service.Roles.Insert($body,$customerId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSAdminRole'
function Remove-GSAdminRole {
    <#
    .SYNOPSIS
    Removes a specific Admin Role or a list of Admin Roles
     
    .DESCRIPTION
    Removes a specific Admin Role or a list of Admin Roles
     
    .PARAMETER RoleId
    The RoleId(s) you would like to remove
     
    .EXAMPLE
    Remove-GSAdminRole -RoleId 9191482342768644,9191482342768642
 
    Removes the admin roles matching the provided Ids
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [int64[]]
        $RoleId
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.rolemanagement'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            'my_customer'
        }
    }
    Process {
        foreach ($Role in $RoleId) {
            try {
                if ($PSCmdlet.ShouldProcess("Deleting Role Id '$Role'")) {
                    Write-Verbose "Deleting Role Id '$Role'"
                    $request = $service.Roles.Delete($customerId,$Role)
                    $request.Execute()
                    Write-Verbose "Role Id '$Role' has been successfully deleted"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSAdminRole'
function Update-GSAdminRole {
    <#
    .SYNOPSIS
    Update an Admin Role
 
    .DESCRIPTION
    Update an Admin Role
 
    .PARAMETER RoleId
    The Id of the role to update
 
    .PARAMETER RoleName
    The name of the role
 
    .PARAMETER RolePrivileges
    The set of privileges that are granted to this role.
 
    .PARAMETER RoleDescription
    A short description of the role.
 
    .EXAMPLE
    Update-GSAdminRole -RoleId 9191482342768644 -RoleName 'Help_Desk_Level2' -RoleDescription 'Help Desk Level 2'
 
    Updates the specified Admin Role with a new name and description
 
    .EXAMPLE
    Get-GSAdminRole | Where-Object {$_.RoleDescription -like "*Help*Desk*"} | Update-GSAdminRole -RoleId 9191482342768644 -RoleName 'Help_Desk_Level2' -RoleDescription 'Help Desk Level 2'
 
    Updates the specified Admin Role's RolePrivileges to match every other Admin Role with Help Desk in the description. Useful for basing a new role off another to add additional permissions on there
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Role')]
    [cmdletbinding()]
    Param
    (

        [parameter(Mandatory = $true,Position = 0)]
        [int64]
        $RoleId,
        [parameter(Mandatory = $false)]
        [String]
        $RoleName,
        [parameter(Mandatory = $false,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Google.Apis.Admin.Directory.directory_v1.Data.Role+RolePrivilegesData[]]
        $RolePrivileges,
        [parameter(Mandatory = $false)]
        [String]
        $RoleDescription
    )
    Begin {
        if ($PSCmdlet.ParameterSetName -eq 'Get') {
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/admin.directory.rolemanagement'
                ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
            }
            $service = New-GoogleService @serviceParams
        }
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            'my_customer'
        }
        $privArray = New-Object 'System.Collections.Generic.List[Google.Apis.Admin.Directory.directory_v1.Data.Role+RolePrivilegesData]'
    }
    Process {
        foreach ($Privilege in $RolePrivileges) {
            $privArray.Add($Privilege)
        }
    }
    End {
        try {
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Role'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$_ -ne 'RoleId' -and $body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    RolePrivileges {
                        $body.RolePrivileges = $privArray
                    }
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            Write-Verbose "Updating Admin Role '$RoleId'"
            $request = $service.Roles.Insert($body,$customerId,$RoleId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSAdminRole'
function Get-GSUserSchema {
    <#
    .SYNOPSIS
    Gets custom user schema info
 
    .DESCRIPTION
    Gets custom user schema info
 
    .PARAMETER SchemaId
    The Id or Name of the user schema you would like to return info for. If excluded, gets the full list of user schemas
 
    .EXAMPLE
    Get-GSUserSchema
 
    Gets the list of custom user schemas
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Schema')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Schema')]
        [String[]]
        $SchemaId
    )
    Begin {
        if ($PSBoundParameters.Keys -contains 'SchemaId') {
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/admin.directory.userschema'
                ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
            }
            $service = New-GoogleService @serviceParams
        }
    }
    Process {
        try {
            if ($PSBoundParameters.Keys -contains 'SchemaId') {
                foreach ($S in $SchemaId) {
                    Write-Verbose "Getting schema Id '$S'"
                    $request = $service.Schemas.Get($Script:PSGSuite.CustomerId,$S)
                    $request.Execute()
                }
            }
            else {
                Get-GSUserSchemaListPrivate @PSBoundParameters
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUserSchema'
function New-GSUserSchema {
    <#
    .SYNOPSIS
    Creates a new user schema
 
    .DESCRIPTION
    Creates a new user schema
 
    .PARAMETER SchemaName
    The name of the schema to create
 
    .PARAMETER Fields
    New schema fields to set
 
    Expects SchemaFieldSpec objects. You can create these with the helper function Add-GSUserSchemaField, i.e.: Add-GSUserSchemaField -FieldName "date" -FieldType DATE -ReadAccessType ADMINS_AND_SELF
 
    .EXAMPLE
    New-GSUserSchema -SchemaName "SDK" -Fields (Add-GSUserSchemaField -FieldName "string" -FieldType STRING -ReadAccessType ADMINS_AND_SELF),(Add-GSUserSchemaField -FieldName "date" -FieldType DATE -ReadAccessType ADMINS_AND_SELF)
 
    This command will create a schema named "SDK" with two fields, "string" and "date", readable by ADMINS_AND_SELF
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Schema')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true)]
        [String]
        $SchemaName,
        [parameter(Mandatory = $true)]
        [Google.Apis.Admin.Directory.directory_v1.Data.SchemaFieldSpec[]]
        $Fields
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.userschema'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Creating schema '$SchemaName'"
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Schema'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            $request = $service.Schemas.Insert($body,$Script:PSGSuite.CustomerId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSUserSchema'
function Remove-GSUserSchema {
    <#
    .SYNOPSIS
    Removes a custom user schema
     
    .DESCRIPTION
    Removes a custom user schema
     
    .PARAMETER SchemaId
    The SchemaId or SchemaName to remove. If excluded, all Custom User Schemas for the customer will be removed
    .EXAMPLE
    Remove-GSUserSchema 2SV
 
    Removes the custom user schema named '2SV' after confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Schema','SchemaKey','SchemaName')]
        [String[]]
        $SchemaId
    )
    Begin {
        if ($PSBoundParameters.Keys -contains 'SchemaName') {
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/admin.directory.user.userschema'
                ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
            }
            $service = New-GoogleService @serviceParams
        }
    }
    Process {
        try {
            if ($PSBoundParameters.Keys -contains 'SchemaId') {
                foreach ($S in $SchemaId) {
                    if ($PSCmdlet.ShouldProcess("Deleting User Schema '$S'")) {
                        Write-Verbose "Deleting User Schema '$S'"
                        $request = $service.Schemas.Delete($Script:PSGSuite.CustomerID,$S)
                        $request.Execute()
                        Write-Verbose "User Schema '$S' has been successfully deleted"
                    }
                }
            }
            else {
                if ($PSCmdlet.ShouldProcess("Deleting ALL User Schemas")) {
                    Write-Verbose "Deleting ALL User Schemas"
                    Get-GSUserSchema -Verbose:$false | ForEach-Object {
                        $request = $service.Schemas.Delete($Script:PSGSuite.CustomerID,$_.SchemaId)
                        $request.Execute()
                        Write-Verbose "User Schema '$($_.SchemaId)' has been successfully deleted"
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSUserSchema'
function Set-GSUserSchema {
    <#
    .SYNOPSIS
    Hard-sets a schema's configuration
 
    .DESCRIPTION
    Hard-sets a schema's configuration
 
    .PARAMETER SchemaId
    The unique Id of the schema to set
 
    .PARAMETER SchemaName
    The new schema name
 
    .PARAMETER Fields
    New schema fields to set
 
    Expects SchemaFieldSpec objects. You can create these with the helper function Add-GSUserSchemaField, i.e.: Add-GSUserSchemaField -FieldName "date" -FieldType DATE -ReadAccessType ADMINS_AND_SELF
 
    .EXAMPLE
    Set-GSUserSchema -SchemaId "9804800jfl08917304j" -SchemaName "SDK_2" -Fields (Add-GSUserSchemaField -FieldName "string2" -FieldType STRING -ReadAccessType ADMINS_AND_SELF)
 
    This command will set the schema Id '9804800jfl08917304j' with the name "SDK_2" and one field "string2" readable by ADMINS_AND_SELF
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Schema')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Schema')]
        [String]
        $SchemaId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $SchemaName,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.SchemaFieldSpec[]]
        $Fields
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.userschema'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Setting schema '$SchemaId'"
            $schemaObj = Get-GSUserSchema -Schema $SchemaId -Verbose:$false
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Schema'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                $body.$prop = $PSBoundParameters[$prop]
            }
            $request = $service.Schemas.Update($body,$Script:PSGSuite.CustomerId,$schemaObj.SchemaId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Set-GSUserSchema'
function Update-GSUserSchema {
    <#
    .SYNOPSIS
    Updates/patches a schema's configuration
 
    .DESCRIPTION
    Updates/patches a schema's configuration
 
    .PARAMETER SchemaId
    The unique Id of the schema to update
 
    .PARAMETER SchemaName
    The new schema name
 
    .PARAMETER Fields
    New schema fields to set
 
    Expects SchemaFieldSpec objects. You can create these with the helper function Add-GSUserSchemaField, i.e.: Add-GSUserSchemaField -FieldName "date" -FieldType DATE -ReadAccessType ADMINS_AND_SELF
 
    .EXAMPLE
    Update-GSUserSchema -SchemaId "9804800jfl08917304j" -SchemaName "SDK_2" -Fields (Add-GSUserSchemaField -FieldName "string2" -FieldType STRING -ReadAccessType ADMINS_AND_SELF)
 
    This command will update the schema Id '9804800jfl08917304j' with the name "SDK_2" and add one field "string2" readable by ADMINS_AND_SELF
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Schema')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Schema')]
        [String]
        $SchemaId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String]
        $SchemaName,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.SchemaFieldSpec[]]
        $Fields
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.userschema'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Updating schema '$SchemaId'"
            $schemaObj = Get-GSUserSchema -Schema $SchemaId -Verbose:$false
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Schema'
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                $body.$prop = $PSBoundParameters[$prop]
            }
            $request = $service.Schemas.Patch($body,$Script:PSGSuite.CustomerId,$schemaObj.SchemaId)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSUserSchema'
function Get-GSChromeOSDevice {
    <#
    .SYNOPSIS
    Gets the list of Chrome OS Devices registered for the user's account
 
    .DESCRIPTION
    Gets the list of Chrome OS Devices registered for the user's account
 
    .PARAMETER ResourceId
    Immutable ID of Chrome OS Device. Gets the list of Chrome OS devices if excluded.
 
    .PARAMETER OrgUnitPath
    The full path of the organizational unit or its unique ID.
 
    .PARAMETER Filter
    Search string in the format given at: http://support.google.com/chromeos/a/bin/answer.py?answer=1698333
 
    .PARAMETER Projection
    Restrict information returned to a set of selected fields.
 
    Acceptable values are:
    * "BASIC": Includes only the basic metadata fields (e.g., deviceId, model, status, type, and status)
    * "FULL": Includes all metadata fields
 
    Defauls to "FULL"
 
    .PARAMETER PageSize
    Page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .PARAMETER OrderBy
    Device property to use for sorting results.
 
    Acceptable values are:
    * "annotatedLocation": Chrome device location as annotated by the administrator.
    * "annotatedUser": Chrome device user as annotated by the administrator.
    * "lastSync": The date and time the Chrome device was last synchronized with the policy settings in the Admin console.
    * "notes": Chrome device notes as annotated by the administrator.
    * "serialNumber": The Chrome device serial number entered when the device was enabled.
    * "status": Chrome device status. For more information, see the chromeosdevices resource.
    * "supportEndDate": Chrome device support end date. This is applicable only for devices purchased directly from Google.
 
    .PARAMETER SortOrder
    Whether to return results in ascending or descending order. Must be used with the OrderBy parameter.
 
    Acceptable values are:
    * "ASCENDING": Ascending order.
    * "DESCENDING": Descending order.
 
    .EXAMPLE
    Get-GSChromeOSDevice
 
    Gets the Chrome OS Device list for the customer
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.ChromeOSDevice')]
    [cmdletbinding(DefaultParameterSetName = "List")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Get")]
        [Alias('Id','Device','DeviceId')]
        [String[]]
        $ResourceId,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('Query')]
        [String]
        $Filter,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [String]
        $OrgUnitPath,
        [parameter(Mandatory = $false)]
        [ValidateSet("BASIC","FULL")]
        [String]
        $Projection = "FULL",
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateRange(1,100)]
        [Alias('MaxResults')]
        [Int]
        $PageSize = 100,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateSet("annotatedLocation","annotatedUser","lastSync","notes","serialNumber","status","supportEndDate")]
        [String]
        $OrderBy,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateSet("ASCENDING","DESCENDING")]
        [String]
        $SortOrder
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.device.chromeos'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            "my_customer"
        }

    }
    Process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                Get {
                    foreach ($dev in $ResourceId) {
                        try {
                            Write-Verbose "Getting Chrome OS Device '$dev'"
                            $request = $service.Chromeosdevices.Get($customerId,$dev)
                            $request.Execute()
                        }
                        catch {
                            if ($ErrorActionPreference -eq 'Stop') {
                                $PSCmdlet.ThrowTerminatingError($_)
                            }
                            else {
                                Write-Error $_
                            }
                        }
                    }
                }
                List {
                    $request = $service.Chromeosdevices.List($customerId)
                    if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                        $PageSize = $Limit
                    }
                    $request.MaxResults = $PageSize
                    if ($PSBoundParameters.Keys -contains 'Filter') {
                        Write-Verbose "Getting Chrome OS Device list for filter '$Filter'"
                        $request.Query = $PSBoundParameters['Filter']
                    }
                    else {
                        Write-Verbose "Getting Chrome OS Device list"
                    }

                    foreach ($key in $PSBoundParameters.Keys | Where-Object {$_ -notin @('PageSize','Filter')}) {
                        if ($request.PSObject.Properties.Name -contains $key) {
                            $request.$key = $PSBoundParameters[$key]
                        }
                    }
                    [int]$i = 1
                    $overLimit = $false
                    do {
                        $result = $request.Execute()
                        $result.Chromeosdevices
                        if ($result.NextPageToken) {
                            $request.PageToken = $result.NextPageToken
                        }
                        [int]$retrieved = ($i + $result.Chromeosdevices.Count) - 1
                        Write-Verbose "Retrieved $retrieved Chrome OS Devices..."
                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                            Write-Verbose "Limit reached: $Limit"
                            $overLimit = $true
                        }
                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                            $newPS = $Limit - $retrieved
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                            $request.MaxResults = $newPS
                        }
                        [int]$i = $i + $result.Chromeosdevices.Count
                    }
                    until ($overLimit -or !$result.NextPageToken)
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSChromeOSDevice'
function Get-GSMobileDevice {
    <#
    .SYNOPSIS
    Gets the list of Mobile Devices registered for the user's account
 
    .DESCRIPTION
    Gets the list of Mobile Devices registered for the user's account
 
    .PARAMETER User
    The user that you would like to retrieve the Mobile Device list for. If no user is specified, it will list all of the Mobile Devices of the CustomerID
 
    .PARAMETER Filter
    Search string in the format given at: http://support.google.com/a/bin/answer.py?hl=en&answer=1408863#search
 
    .PARAMETER Projection
    Restrict information returned to a set of selected fields.
 
    Acceptable values are:
    * "BASIC": Includes only the basic metadata fields (e.g., deviceId, model, status, type, and status)
    * "FULL": Includes all metadata fields
 
    Defauls to "FULL"
 
    .PARAMETER PageSize
    Page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .PARAMETER OrderBy
    Device property to use for sorting results.
 
    Acceptable values are:
    * "deviceId": The serial number for a Google Sync mobile device. For Android devices, this is a software generated unique identifier.
    * "email": The device owner's email address.
    * "lastSync": Last policy settings sync date time of the device.
    * "model": The mobile device's model.
    * "name": The device owner's user name.
    * "os": The device's operating system.
    * "status": The device status.
    * "type": Type of the device.
 
    .PARAMETER SortOrder
    Whether to return results in ascending or descending order. Must be used with the OrderBy parameter.
 
    Acceptable values are:
    * "ASCENDING": Ascending order.
    * "DESCENDING": Descending order.
 
    .EXAMPLE
    Get-GSMobileDevice
 
    Gets the Mobile Device list for the AdminEmail
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.MobileDevice')]
    [cmdletbinding(DefaultParameterSetName = "User")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "User")]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User,
        [parameter(Mandatory = $false,ParameterSetName = "Query",Position = 0)]
        [Alias('Query')]
        [String]
        $Filter,
        [parameter(Mandatory = $false)]
        [ValidateSet("BASIC","FULL")]
        [String]
        $Projection = "FULL",
        [parameter(Mandatory = $false)]
        [ValidateRange(1,1000)]
        [Int]
        $PageSize = 1000,
        [parameter(Mandatory = $false)]
        [Alias('First')]
        [Int]
        $Limit = 0,
        [parameter(Mandatory = $false)]
        [ValidateSet("deviceId","email","lastSync","model","name","os","status","type")]
        [String]
        $OrderBy,
        [parameter(Mandatory = $false)]
        [ValidateSet("Ascending","Descending")]
        [String]
        $SortOrder
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.device.mobile'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            "my_customer"
        }
    }
    Process {
        try {
            $request = $service.Mobiledevices.List($customerId)
            if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                $PageSize = $Limit
            }
            $request.MaxResults = $PageSize
            [int]$i = 1
            $overLimit = $false
            switch ($PSCmdlet.ParameterSetName) {
                User {
                    if ($User) {
                        foreach ($U in $User) {
                            if ($U -ceq 'me') {
                                $U = $Script:PSGSuite.AdminEmail
                            }
                            elseif ($U -notlike "*@*.*") {
                                $U = "$($U)@$($Script:PSGSuite.Domain)"
                            }
                            $Filter = "email:`"$U`""
                            $request.Query = $Filter
                            Write-Verbose "Getting Mobile Device list for User '$U'"
                            do {
                                $result = $request.Execute()
                                $result.Mobiledevices
                                if ($result.NextPageToken) {
                                    $request.PageToken = $result.NextPageToken
                                }
                                [int]$retrieved = ($i + $result.Mobiledevices.Count) - 1
                                Write-Verbose "Retrieved $retrieved Mobile Devices..."
                                if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                                    Write-Verbose "Limit reached: $Limit"
                                    $overLimit = $true
                                }
                                elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                                    $newPS = $Limit - $retrieved
                                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                                    $request.MaxResults = $newPS
                                }
                                [int]$i = $i + $result.Mobiledevices.Count
                            }
                            until ($overLimit -or !$result.NextPageToken)
                        }
                    }
                    else {
                        Write-Verbose "Getting Mobile Device list for customer '$($script:PSGSuite.CustomerID)'"
                        do {
                            $result = $request.Execute()
                            $result.Mobiledevices
                            if ($result.NextPageToken) {
                                $request.PageToken = $result.NextPageToken
                            }
                            [int]$retrieved = ($i + $result.Mobiledevices.Count) - 1
                            Write-Verbose "Retrieved $retrieved Mobile Devices..."
                            if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                                Write-Verbose "Limit reached: $Limit"
                                $overLimit = $true
                            }
                            elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                                $newPS = $Limit - $retrieved
                                Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                                $request.MaxResults = $newPS
                            }
                            [int]$i = $i + $result.Mobiledevices.Count
                        }
                        until ($overLimit -or !$result.NextPageToken)
                    }
                }
                Query {
                    $request.Query = $Filter
                    Write-Verbose "Getting Mobile Device list for filter '$Filter'"
                    do {
                        $result = $request.Execute()
                        $result.Mobiledevices
                        if ($result.NextPageToken) {
                            $request.PageToken = $result.NextPageToken
                        }
                        [int]$retrieved = ($i + $result.Mobiledevices.Count) - 1
                        Write-Verbose "Retrieved $retrieved Mobile Devices..."
                        if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                            Write-Verbose "Limit reached: $Limit"
                            $overLimit = $true
                        }
                        elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                            $newPS = $Limit - $retrieved
                            Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                            $request.MaxResults = $newPS
                        }
                        [int]$i = $i + $result.Mobiledevices.Count
                    }
                    until ($overLimit -or !$result.NextPageToken)
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSMobileDevice'
function Get-GSUserASP {
    <#
    .SYNOPSIS
    Gets Application Specific Passwords for a user
 
    .DESCRIPTION
    Gets Application Specific Passwords for a user
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to get info for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .PARAMETER CodeId
    The ID of the ASP you would like info for. If excluded, returns the full list of ASP's for the user
 
    .EXAMPLE
    Get-GSUserASP
 
    Gets the list of Application Specific Passwords for the user
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Asp')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,Position = 1,ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $CodeId
    )
    Begin {
        if ($PSBoundParameters.Keys -contains 'CodeId') {
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
                ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
            }
            $service = New-GoogleService @serviceParams
        }
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                if ($PSBoundParameters.Keys -contains 'CodeId') {
                    Write-Verbose "Getting ASP '$CodeId' for User '$U'"
                    $request = $service.Asps.Get($U,$CodeId)
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                }
                else {
                    $PSBoundParameters['User'] = $U
                    Get-GSUserASPListPrivate @PSBoundParameters
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUserASP'
function Get-GSUserToken {
    <#
    .SYNOPSIS
    Gets security tokens for a user
 
    .DESCRIPTION
    Gets security tokens for a user
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to get info for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .PARAMETER ClientId
    The Id of the client you are trying to get token info for. If excluded, gets the full list of tokens for the user
 
    .EXAMPLE
    Get-GSUserToken -ClientId "Google Chrome"
 
    Gets the token info for "Google Chrome" for the AdminEmail user
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Token')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,Position = 1,ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ClientId
    )
    Begin {
        if ($PSBoundParameters.Keys -contains 'ClientId') {
            $serviceParams = @{
                Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
                ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
            }
            $service = New-GoogleService @serviceParams
        }
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                if ($PSBoundParameters.Keys -contains 'ClientId') {
                    Write-Verbose "Getting Token '$ClientId' for User '$U'"
                    $request = $service.Tokens.Get($U,$ClientId)
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                }
                else {
                    $PSBoundParameters['User'] = $U
                    Get-GSUserTokenListPrivate @PSBoundParameters
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUserToken'
function Get-GSUserVerificationCodes {
    <#
    .SYNOPSIS
    Gets the 2-Step Verification Codes for the user
 
    .DESCRIPTION
    Gets the 2-Step Verification Codes for the user
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to get info for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .EXAMPLE
    Get-GSUserVerificationCodes
 
    Gets the Verification Codes for AdminEmail user
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.VerificationCode')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Getting Verification Code list for User '$U'"
                $request = $service.VerificationCodes.List($U)
                $request.Execute() | Select-Object -ExpandProperty Items | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUserVerificationCodes'
function New-GSUserVerificationCodes {
    <#
    .SYNOPSIS
    Generates new verification codes for the user
 
    .DESCRIPTION
    Generates new verification codes for the user
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to get info for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config
 
    .EXAMPLE
    New-GSUserVerificationCodes -User me
 
    Generates new verification codes for the AdminEmail user
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.VerificationCode')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Generating new verification codes for user '$U'"
                $request = $service.VerificationCodes.Generate($U)
                $request.Execute()
                Write-Verbose "New verification codes successfully generated for user '$U'"
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSUserVerificationCodes'
function Remove-GSMobileDevice {
    <#
    .SYNOPSIS
    Removes a mobile device from Device Management
 
    .DESCRIPTION
    Removes a mobile device from Device Management
 
    .PARAMETER ResourceID
    The unique Id of the mobile device you would like to remove
 
    .EXAMPLE
    Remove-GSMobileDevice -ResourceId 'AFiQxQ8Qgd-rouSmcd2UnuvhYV__WXdacTgJhPEA1QoQJrK1hYbKJXm-8JFlhZOjBF4aVbhleS2FVQk5lI069K2GULpteTlLVpKLJFSLSL'
 
    Removes the mobile device with the specified Id
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $ResourceId
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.device.mobile'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($R in $ResourceId) {
                if ($PSCmdlet.ShouldProcess("Removing Mobile Device '$R'")) {
                    Write-Verbose "Removing Mobile Device '$R'"
                    $request = $service.Mobiledevices.Delete($Script:PSGSuite.CustomerID,$R)
                    $request.Execute()
                    Write-Verbose "Mobile Device '$R' has been successfully removed"
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSMobileDevice'
function Remove-GSUserASP {
    <#
    .SYNOPSIS
    Removes an Application Specific Password for a user
 
    .DESCRIPTION
    Removes an Application Specific Password for a user
 
    .PARAMETER User
    The user to remove ASPs ValueFromPipeline
 
    .PARAMETER CodeId
    The ASP Code Id to remove. If excluded, all ASPs for the user will be removed
 
    .EXAMPLE
    Remove-GSUserASP -User joe
 
    Removes *ALL* ASPs from joe@domain.com's account after confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [String[]]
        $CodeId
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                if ($PSBoundParameters.Keys -contains 'CodeId') {
                    foreach ($C in $CodeId) {
                        if ($PSCmdlet.ShouldProcess("Deleting ASP CodeId '$C' for user '$U'")) {
                            Write-Verbose "Deleting ASP CodeId '$C' for user '$U'"
                            $request = $service.Asps.Delete($U,$C)
                            $request.Execute()
                            Write-Verbose "ASP CodeId '$C' has been successfully deleted for user '$U'"
                        }
                    }
                }
                else {
                    if ($PSCmdlet.ShouldProcess("Deleting ALL ASPs for user '$U'")) {
                        Write-Verbose "Deleting ALL ASPs for user '$U'"
                        Get-GSUserASP -User $U -Verbose:$false | ForEach-Object {
                            $request = $service.Asps.Delete($U,$_.CodeId)
                            $request.Execute()
                            Write-Verbose "ASP CodeId '$($_.CodeId)' has been successfully deleted for user '$U'"
                        }
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSUserASP'
function Remove-GSUserToken {
    <#
    .SYNOPSIS
    Removes a security token from a user
 
    .DESCRIPTION
    Removes a security token from a user
 
    .PARAMETER User
    The user to remove the security token from
 
    .PARAMETER ClientID
    The client Id of the security token. If excluded, all security tokens for the user are removed
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [String[]]
        $ClientID
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                if ($PSBoundParameters.Keys -contains 'ClientID') {
                    foreach ($C in $ClientID) {
                        if ($PSCmdlet.ShouldProcess("Deleting Token ClientID '$C' for user '$U'")) {
                            Write-Verbose "Deleting Token ClientID '$C' for user '$U'"
                            $request = $service.Tokens.Delete($U,$C)
                            $request.Execute()
                            Write-Verbose "Token ClientID '$C' has been successfully deleted for user '$U'"
                        }
                    }
                }
                else {
                    if ($PSCmdlet.ShouldProcess("Deleting ALL tokens for user '$U'")) {
                        Write-Verbose "Deleting ALL tokens for user '$U'"
                        Get-GSUserToken -User $U -Verbose:$false | ForEach-Object {
                            $request = $service.Tokens.Delete($U,$_.ClientID)
                            $request.Execute()
                            Write-Verbose "Token ClientID '$($_.ClientID)' has been successfully deleted for user '$U'"
                        }
                    }
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Remove-GSUserToken'
function Revoke-GSUserVerificationCodes {
    <#
    .SYNOPSIS
    Revokes/invalidates Verification Codes for the user
     
    .DESCRIPTION
    Revokes/invalidates Verification Codes for the user
     
    .PARAMETER User
    The user to revoke verification codes from
     
    .EXAMPLE
    Revoke-GSUserVerificationCodes -User me -Confirm:$false
 
    Invalidates the verification codes for the AdminEmail user, skipping confirmation
    #>

    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.security'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Invalidating verification codes for user '$U'"
                $request = $service.VerificationCodes.Invalidate($U)
                $request.Execute()
                Write-Verbose "Verification codes successfully invalidated for user '$U'"
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}
Export-ModuleMember -Function 'Revoke-GSUserVerificationCodes'
function Update-GSChromeOSDevice {
    <#
    .SYNOPSIS
    Updates a ChromeOS device
 
    .DESCRIPTION
    Updates a ChromeOS device
 
    .PARAMETER ResourceId
    The unique ID of the device you would like to update.
 
    .PARAMETER Action
    Action to be taken on the Chrome OS device.
 
    Acceptable values are:
    * "deprovision": Remove a device from management that is no longer active, being resold, or is being submitted for return / repair, use the deprovision action to dissociate it from management.
    * "disable": If you believe a device in your organization has been lost or stolen, you can disable the device so that no one else can use it. When a device is disabled, all the user can see when turning on the Chrome device is a screen telling them that it’s been disabled, and your desired contact information of where to return the device.
        Note: Configuration of the message to appear on a disabled device must be completed within the admin console.
    "reenable": Re-enable a disabled device when a misplaced device is found or a lost device is returned. You can also use this feature if you accidentally mark a Chrome device as disabled.
        Note: The re-enable action can only be performed on devices marked as disabled.
 
    .PARAMETER DeprovisionReason
    Only used when the action is deprovision. With the deprovision action, this field is required.
 
    Note: The deprovision reason is audited because it might have implications on licenses for perpetual subscription customers.
 
    Acceptable values are:
    * "different_model_replacement": Use if you're upgrading or replacing your device with a newer model of the same device.
    * "retiring_device": Use if you're reselling, donating, or permanently removing the device from use.
    * "same_model_replacement": Use if a hardware issue was encountered on a device and it is being replaced with the same model or a like-model replacement from a repair vendor / manufacturer.
 
    .PARAMETER AnnotatedAssetId
    The asset identifier as noted by an administrator or specified during enrollment.
 
    .PARAMETER AnnotatedLocation
    The address or location of the device as noted by the administrator. Maximum length is 200 characters. Empty values are allowed.
 
    .PARAMETER AnnotatedUser
    The user of the device as noted by the administrator. Maximum length is 100 characters. Empty values are allowed.
 
    .PARAMETER Notes
    Notes about this device added by the administrator. This property can be searched with the list method's query parameter. Maximum length is 500 characters. Empty values are allowed.
 
    .PARAMETER OrgUnitPath
    Full path of the target organizational unit or its ID that you would like to move the device to.
 
    .EXAMPLE
    Update-GSChromeOSDevice -ResourceId 'AFiQxQ8Qgd-rouSmcd2UnuvhYV__WXdacTgJhPEA1QoQJrK1hYbKJXm-8JFlhZOjBF4aVbhleS2FVQk5lI069K2GULpteTlLVpKLJFSLSL' -Action deprovision -DeprovisionReason retiring_device
 
    Deprovisions the retired ChromeOS device
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.ChromeOSDevice')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias('Id','Device','DeviceId')]
        [String[]]
        $ResourceId,
        [parameter(Mandatory = $false)]
        [ValidateSet('deprovision','disable','reenable')]
        [String]
        $Action,
        [parameter(Mandatory = $false)]
        [ValidateSet('different_model_replacement','retiring_device','same_model_replacement')]
        [String]
        $DeprovisionReason,
        [parameter(Mandatory = $false)]
        [String]
        $AnnotatedAssetId,
        [parameter(Mandatory = $false)]
        [AllowNull()]
        [String]
        $AnnotatedLocation,
        [parameter(Mandatory = $false)]
        [AllowNull()]
        [String]
        $AnnotatedUser,
        [parameter(Mandatory = $false)]
        [AllowNull()]
        [String]
        $Notes,
        [parameter(Mandatory = $false)]
        [String]
        $OrgUnitPath

    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.device.chromeos'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            "my_customer"
        }
        $bodyUpdated = $false
    }
    Process {
        $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.ChromeOsDevice'
        foreach ($key in $PSBoundParameters.Keys | Where-Object {$_ -in @('Action','OrgUnitPath') -or $_ -in $body.PSObject.Properties.Name}) {
            switch ($key) {
                Action {
                    try {
                        $actionBody = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.ChromeOSDeviceAction' -Property @{
                            Action = $Action
                        }
                        if ($PSBoundParameters.Keys -contains 'DeprovisionReason') {
                            $actionBody.DeprovisionReason = $PSBoundParameters['DeprovisionReason']
                        }
                        foreach ($dev in $ResourceId) {
                            Write-Verbose "Updating Chrome OS Device '$dev' with Action '$Action'"
                            $request = $service.Chromeosdevices.Action($actionBody,$customerId,$dev)
                            $request.Execute()
                            Write-Verbose "Chrome OS Device was successfully updated"
                        }
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
                OrgUnitPath {
                    try {
                        Move-GSChromeOSDevice -ResourceId $ResourceId -OrgUnitPath $OrgUnitPath
                    }
                    catch {
                        if ($ErrorActionPreference -eq 'Stop') {
                            $PSCmdlet.ThrowTerminatingError($_)
                        }
                        else {
                            Write-Error $_
                        }
                    }
                }
                default {
                    if ($key -in $body.PSObject.Properties.Name) {
                        $bodyUpdated = $true
                        $body.$key = $PSBoundParameters[$key]
                    }
                }
            }
        }
        if ($bodyUpdated) {
            foreach ($dev in $ResourceId) {
                try {
                    Write-Verbose "Updating Chrome OS Device '$dev'"
                    $request = $service.Chromeosdevices.Patch($body,$customerId,$dev)
                    $request.Execute()
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSChromeOSDevice'
function Update-GSMobileDevice {
    <#
    .SYNOPSIS
    Updates a mobile device with the action specified
 
    .DESCRIPTION
    Updates a mobile device with the action specified
 
    .PARAMETER ResourceID
    The unique Id of the mobile device you would like to update
 
    .PARAMETER Action
    The action to be performed on the device.
 
    Acceptable values are:
    * "admin_account_wipe": Remotely wipes only G Suite data from the device. See the administration help center for more information.
    * "admin_remote_wipe": Remotely wipes all data on the device. See the administration help center for more information.
    * "approve": Approves the device. If you've selected Enable device activation, devices that register after the device activation setting is enabled will need to be approved before they can start syncing with your domain. Enabling device activation forces the device user to install the Device Policy app to sync with G Suite.
    * "block": Blocks access to G Suite data (mail, calendar, and contacts) on the device. The user can still access their mail, calendar, and contacts from a desktop computer or mobile browser.
    * "cancel_remote_wipe_then_activate": Cancels a remote wipe of the device and then reactivates it.
    * "cancel_remote_wipe_then_block": Cancels a remote wipe of the device and then blocks it.
 
    .EXAMPLE
    Update-GSMobileDevice -ResourceId 'AFiQxQ8Qgd-rouSmcd2UnuvhYV__WXdacTgJhPEA1QoQJrK1hYbKJXm-8JFlhZOjBF4aVbhleS2FVQk5lI069K2GULpteTlLVpKLJFSLSL' -Action approve
 
    Approves the mobile device with the specified Id
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.MobileDevice')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $ResourceId,
        [parameter(Mandatory = $true)]
        [ValidateSet('admin_account_wipe','admin_remote_wipe','approve','block','cancel_remote_wipe_then_activate','cancel_remote_wipe_then_block')]
        [String]
        $Action

    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.device.mobile'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $customerId = if ($Script:PSGSuite.CustomerID) {
            $Script:PSGSuite.CustomerID
        }
        else {
            "my_customer"
        }
        $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.MobileDeviceAction' -Property @{
            Action = $Action
        }
    }
    Process {
        try {
            foreach ($R in $ResourceId) {
                Write-Verbose "Updating Mobile Device '$R' with Action '$Action'"
                $request = $service.Mobiledevices.Action($body,$customerId,$R)
                $request.Execute()
                Write-Verbose "Mobile Device was successfully updated"
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSMobileDevice'
function Add-GSSheetValues {
    <#
    .SYNOPSIS
    Append data after a table of data in a sheet. This uses the native `Spreadsheets.Values.Append()` method instead of `BatchUpdate()`.
 
    .DESCRIPTION
    Append data after a table of data in a sheet. This uses the native `Spreadsheets.Values.Append()` method instead of `BatchUpdate()`. See the following link for more information: https://github.com/scrthq/PSGSuite/issues/216
 
    .PARAMETER SpreadsheetId
    The unique Id of the SpreadSheet to Append data to if updating an existing Sheet
 
    .PARAMETER NewSheetTitle
    The title of the new SpreadSheet to be created
 
    .PARAMETER Array
    Array of objects/strings/ints to append to the SpreadSheet
 
    .PARAMETER Value
    A single value to update 1 cell with.
 
    .PARAMETER SheetName
    The name of the Sheet to add the data to. If excluded, defaults to Sheet Id '0'. If a new SpreadSheet is being created, this is set to 'Sheet1' to prevent error
 
    .PARAMETER Style
    The table style you would like to export the data as
 
    Available values are:
    * "Standard": headers are on Row 1, table rows are added as subsequent rows (Default)
    * "Horizontal": headers are on Column A, table rows are added as subsequent columns
 
    .PARAMETER Range
    The input range is used to search for existing data and find a "table" within that range. Values are appended to the next row of the table, starting with the first column of the table.
 
    .PARAMETER Append
    If $true, skips adding headers to the Sheet
 
    .PARAMETER User
    The primary email of the user that had at least Edit rights to the target Sheet
 
    Defaults to the AdminEmail user
 
    .PARAMETER ValueInputOption
    How the input data should be interpreted
 
    Available values are:
    * "INPUT_VALUE_OPTION_UNSPECIFIED"
    * "RAW"
    * "USER_ENTERED"
 
    .PARAMETER InsertDataOption
    How the input data should be inserted.
 
    Available values are:
    * "OVERWRITE"
    * "INSERTROWS"
 
    .PARAMETER ResponseValueRenderOption
    Determines how values in the response should be rendered. The default render option is FORMATTEDVALUE.
 
    Available values are:
    * "FORMATTEDVALUE"
    * "UNFORMATTEDVALUE"
    * "FORMULA"
 
    .PARAMETER ResponseDateTimeRenderOption
    Determines how dates, times, and durations in the response should be rendered. This is ignored if responseValueRenderOption is FORMATTEDVALUE. The default dateTime render option is SERIALNUMBER.
 
    Available values are:
    * "SERIALNUMBER"
    * "FORMATTEDSTRING"
 
    .PARAMETER IncludeValuesInResponse
    Determines if the update response should include the values of the cells that were updated. By default, responses do not include the updated values
 
    .PARAMETER Launch
    If $true, opens the new SpreadSheet Url in your default browser
 
    .EXAMPLE
    Add-GSSheetValues -SpreadsheetId $sheetId -Array $items -Range 'A:Z'
 
    Finds the first empty row on the Sheet and appends the $items array (including header row) to it starting at that row.
 
    .EXAMPLE
    Add-GSSheetValues -SpreadsheetId $sheetId -Array $items -Range 'A:Z' -Append
 
    Finds the first empty row on the Sheet and appends the $items array (excludes header row due to -Append switch) to it starting at that row.
    #>

    [OutputType('Google.Apis.Sheets.v4.Data.Spreadsheet')]
    [cmdletbinding(DefaultParameterSetName = "CreateNewSheetArray")]
    Param
    (
        [parameter(Mandatory = $true, Position = 0, ParameterSetName = "UseExistingArray")]
        [parameter(Mandatory = $true, Position = 0, ParameterSetName = "UseExistingValue")]
        [String]
        $SpreadsheetId,
        [parameter(Mandatory = $false, Position = 0, ParameterSetName = "CreateNewSheetArray")]
        [parameter(Mandatory = $false, Position = 0, ParameterSetName = "CreateNewSheetValue")]
        [String]
        $NewSheetTitle,
        [parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true, ParameterSetName = "UseExistingArray")]
        [parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true, ParameterSetName = "CreateNewSheetArray")]
        [object[]]
        $Array,
        [parameter(Mandatory = $true, Position = 1, ParameterSetName = "UseExistingValue")]
        [parameter(Mandatory = $true, Position = 1, ParameterSetName = "CreateNewSheetValue")]
        [string]
        $Value,
        [parameter(Mandatory = $false)]
        [String]
        $SheetName,
        [parameter(Mandatory = $false, ParameterSetName = "UseExistingArray")]
        [parameter(Mandatory = $false, ParameterSetName = "CreateNewSheetArray")]
        [ValidateSet('Standard', 'Horizontal')]
        [String]
        $Style = "Standard",
        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('SpecifyRange')]
        [string]
        $Range,
        [parameter(Mandatory = $false)]
        [switch]
        $Append,
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner', 'PrimaryEmail', 'UserKey', 'Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+AppendRequest+ValueInputOptionEnum]
        $ValueInputOption = [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+AppendRequest+ValueInputOptionEnum]::RAW,
        [parameter(Mandatory = $false)]
        [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+AppendRequest+InsertDataOptionEnum]
        $InsertDataOption = [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+AppendRequest+InsertDataOptionEnum]::OVERWRITE,
        [parameter(Mandatory = $false)]
        [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+AppendRequest+ResponseValueRenderOptionEnum]
        $ResponseValueRenderOption = [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+AppendRequest+ResponseValueRenderOptionEnum]::FORMATTEDVALUE,
        [parameter(Mandatory = $false)]
        [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+AppendRequest+ResponseDateTimeRenderOptionEnum]
        $ResponseDateTimeRenderOption = [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+AppendRequest+ResponseDateTimeRenderOptionEnum]::FORMATTEDSTRING,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeValuesInResponse,
        [parameter(Mandatory = $false)]
        [Alias('Open')]
        [Switch]
        $Launch

    )
    Begin {
        $values = New-Object 'System.Collections.Generic.List[System.Collections.Generic.IList[Object]]'
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Sheets.v4.SheetsService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($Value) {
                $finalArray = $([pscustomobject]@{Value = "$Value" })
                $Append = $true
            }
            else {
                if (!$contentType) {
                    $contentType = $Array[0].PSObject.TypeNames[0]
                }
                $finalArray = @()
                if ($contentType -eq 'System.String' -or $contentType -like "System.Int*") {
                    $Append = $true
                    foreach ($item in $Array) {
                        $finalArray += $([pscustomobject]@{Value = $item })
                    }
                }
                else {
                    foreach ($item in $Array) {
                        $finalArray += $item
                    }
                }
            }
            if (!$Append) {
                $propArray = New-Object 'System.Collections.Generic.List[Object]'
                $finalArray[0].PSObject.Properties.Name | ForEach-Object {
                    $propArray.Add($_)
                }
                $values.Add([System.Collections.Generic.IList[Object]]$propArray)
                $Append = $true
            }
            foreach ($object in $finalArray) {
                $valueArray = New-Object 'System.Collections.Generic.List[Object]'
                $object.PSobject.Properties.Value | ForEach-Object {
                    $valueArray.Add($_)
                }
                $values.Add([System.Collections.Generic.IList[Object]]$valueArray)
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
    End {
        try {
            if ($PSCmdlet.ParameterSetName -like "CreateNewSheet*") {
                if ($NewSheetTitle) {
                    Write-Verbose "Creating new spreadsheet titled: $NewSheetTitle"
                }
                else {
                    Write-Verbose "Creating new untitled spreadsheet"
                }
                $sheet = New-GSSheet -Title $NewSheetTitle -User $User -Verbose:$false
                $SpreadsheetId = $sheet.SpreadsheetId
                $SpreadsheetUrl = $sheet.SpreadsheetUrl
                $SheetName = 'Sheet1'
                Write-Verbose "New spreadsheet ID: $SpreadsheetId"
            }
            else {
                $sheet = Get-GSSheetInfo -SpreadsheetId $SpreadsheetId -User $User -Verbose:$false
                $SpreadsheetUrl = $sheet.SpreadsheetUrl
            }
            if ($SheetName) {
                if ($Range -like "'*'!*") {
                    throw "SpecifyRange formatting error! When using the SheetName parameter, please exclude the SheetName when formatting the SpecifyRange value (i.e. 'A1:Z1000')"
                }
                elseif ($Range) {
                    $Range = "'$($SheetName)'!$Range"
                }
                else {
                    $Range = "$SheetName"
                }
            }
            $body = (New-Object 'Google.Apis.Sheets.v4.Data.ValueRange' -Property @{
                    Range          = $Range
                    MajorDimension = "$(if($Style -eq 'Horizontal'){'COLUMNS'}else{'ROWS'})"
                    Values         = [System.Collections.Generic.IList[System.Collections.Generic.IList[Object]]]$values
                })

            $request = $service.Spreadsheets.Values.Append($body, $SpreadsheetId, $Range)
            $request.valueInputOption = $ValueInputOption;
            $request.insertDataOption = $InsertDataOption;
            $request.IncludeValuesInResponse = $IncludeValuesInResponse;
            $request.responseValueRenderOption = $ResponseValueRenderOption;
            $request.responseDateTimeRenderOption = $ResponseDateTimeRenderOption;

            Write-Verbose "Appending to Range '$Range' on Spreadsheet '$SpreadsheetId' for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru | Add-Member -MemberType NoteProperty -Name 'SpreadsheetUrl' -Value $SpreadsheetUrl -PassThru
            if ($Launch) {
                Write-Verbose "Launching new spreadsheet at $SpreadsheetUrl"
                Start-Process $SpreadsheetUrl
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Add-GSSheetValues'
function Clear-GSSheet {
    <#
    .SYNOPSIS
    Clears a Sheet
 
    .DESCRIPTION
    Clears a Sheet
 
    .PARAMETER SpreadsheetId
    The unique Id of the SpreadSheet
 
    .PARAMETER SheetName
    The name of the Sheet (tab) to clear
 
    .PARAMETER Range
    The specific range to clear. If excluded, clears the entire Sheet
 
    .PARAMETER User
    The primary email of the user who has Edit rights to the target Range/Sheet
 
    .PARAMETER Raw
    If $true, return the raw response, otherwise, return a flattened response for readability
 
    .EXAMPLE
    Clear-GSSheet -SpreadsheetId '1ZVdewVhy-VtVLyGL1lk2kgvySIF_bCfJA6ggn7obGh2U' -SheetName 2017
 
    Clears the Sheet '2017' located on the SpreadSheet Id provided
    #>

    [OutputType('Google.Apis.Sheets.v4.Data.Spreadsheet')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $SpreadsheetId,
        [parameter(Mandatory = $false)]
        [String]
        $SheetName,
        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('SpecifyRange')]
        [string]
        $Range,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Sheets.v4.SheetsService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($SheetName) {
                if ($Range -like "'*'!*") {
                    throw "SpecifyRange formatting error! When using the SheetName parameter, please exclude the SheetName when formatting the SpecifyRange value (i.e. 'A1:Z1000')"
                }
                elseif ($Range) {
                    $Range = "'$($SheetName)'!$Range"
                }
                else {
                    $Range = "$SheetName"
                }
            }
            $body = New-Object 'Google.Apis.Sheets.v4.Data.ClearValuesRequest'
            $request = $service.Spreadsheets.Values.Clear($body,$SpreadsheetId,$Range)
            Write-Verbose "Clearing range '$Range' on Sheet '$SpreadsheetId' for user '$User'"
            $response = $request.Execute()
            if (!$Raw) {
                $response = $response | Select-Object @{N = "Title";E = {$_.properties.title}},@{N = "MaxRows";E = {[int]($_.sheets.properties.gridProperties.rowCount | Sort-Object | Select-Object -Last 1)}},@{N = "MaxColumns";E = {[int]($_.sheets.properties.gridProperties.columnCount | Sort-Object | Select-Object -Last 1)}},*
            }
            $response | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Clear-GSSheet'
function Copy-GSSheet {
    <#
    .SYNOPSIS
    Copies a Sheet from one SpreadSheet to another
 
    .DESCRIPTION
    Copies a Sheet from one SpreadSheet to another
 
    .PARAMETER SourceSpreadsheetId
    The unique Id of the SpreadSheet to copy the Sheet from
 
    .PARAMETER SourceSheetId
    The Id of the Sheet to copy
 
    .PARAMETER DestinationSpreadsheetId
    The target SpreadSheet to copy the Sheet to
 
    .PARAMETER NewSheetTitle
    The new title for the new SpreadhSheet to create if not copying to a Destination Sheet
 
    .PARAMETER User
    The primary email of the user who has at least Edit rights to both the Source SpreadSheet and Destination SpreadSheet
 
    .PARAMETER Raw
    If $true, return the raw response, otherwise, return a flattened response for readability
 
    .EXAMPLE
    Copy-GSSheet -SourceSpreadsheetId '1ZVdewVhy-VtVLyGLhClkj8234ljk_fJA6ggn7obGh2U' -SourceSheetId 2017 -NewSheetTitle '2017 Archive'
 
    Copies the Sheet '2017' from the SourceSpreadsheet provided onto a new SpreadSheet named '2017 Archive'
    #>

    [cmdletbinding(DefaultParameterSetName = "CreateNewSheet")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0)]
        [String]
        $SourceSpreadsheetId,
        [parameter(Mandatory = $true,Position = 1)]
        [String]
        $SourceSheetId,
        [parameter(Mandatory = $true,Position = 2,ParameterSetName = "UseExisting")]
        [String]
        $DestinationSpreadsheetId,
        [parameter(Mandatory = $false,ParameterSetName = "CreateNewSheet")]
        [Alias('SheetTitle')]
        [String]
        $NewSheetTitle,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Sheets.v4.SheetsService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($PSCmdlet.ParameterSetName -eq "CreateNewSheet") {
                if ($NewSheetTitle) {
                    Write-Verbose "Creating new spreadsheet titled: $NewSheetTitle"
                }
                else {
                    Write-Verbose "Creating new untitled spreadsheet"
                }
                $DestinationSpreadsheetId = New-GSSheet -Title $NewSheetTitle -User $User -Verbose:$false | Select-Object -ExpandProperty SpreadsheetId
                Write-Verbose "New spreadsheet ID: $DestinationSpreadsheetId"
            }
            $body = New-Object 'Google.Apis.Sheets.v4.Data.CopySheetToAnotherSpreadsheetRequest' -Property @{
                DestinationSpreadsheetId = $DestinationSpreadsheetId
            }
            Write-Verbose "Copying Sheet '$SourceSheetId' from Spreadsheet '$SourceSpreadsheetId' to Spreadsheet '$DestinationSpreadsheetId' for user '$User'"
            $request = $service.Spreadsheets.Sheets.CopyTo($body,$SourceSpreadsheetId,$SourceSheetId)
            $response = $request.Execute()
            if (!$Raw) {
                $response = $response | Select-Object @{N = "Title";E = {$_.properties.title}},@{N = "MaxRows";E = {[int]($_.sheets.properties.gridProperties.rowCount | Sort-Object | Select-Object -Last 1)}},@{N = "MaxColumns";E = {[int]($_.sheets.properties.gridProperties.columnCount | Sort-Object | Select-Object -Last 1)}},*
            }
            $response | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Copy-GSSheet'
function Export-GSSheet {
    <#
    .SYNOPSIS
    Updates a Sheet's values
 
    .DESCRIPTION
    Updates a Sheet's values. Accepts either an Array of objects/strings/ints or a single value
 
    .PARAMETER SpreadsheetId
    The unique Id of the SpreadSheet to update if updating an existing Sheet
 
    .PARAMETER NewSheetTitle
    The title of the new SpreadSheet to be created
 
    .PARAMETER Array
    Array of objects/strings/ints to add to the SpreadSheet
 
    .PARAMETER Value
    A single value to update 1 cell with. Useful if you are tracking the last time updated in a specific cell during a job that updates Sheets
 
    .PARAMETER SheetName
    The name of the Sheet to add the data to. If excluded, defaults to Sheet Id '0'. If a new SpreadSheet is being created, this is set to 'Sheet1' to prevent error
 
    .PARAMETER Style
    The table style you would like to export the data as
 
    Available values are:
    * "Standard": headers are on Row 1, table rows are added as subsequent rows (Default)
    * "Horizontal": headers are on Column A, table rows are added as subsequent columns
 
    .PARAMETER Range
    The specific range to add the value(s) to. If using the -Value parameter, set this to the specific cell you would like to set the value of
 
    .PARAMETER Append
    If $true, skips adding headers to the Sheet
 
    .PARAMETER User
    The primary email of the user that had at least Edit rights to the target Sheet
 
    Defaults to the AdminEmail user
 
    .PARAMETER ValueInputOption
    How the input data should be interpreted
 
    Available values are:
    * "INPUT_VALUE_OPTION_UNSPECIFIED"
    * "RAW"
    * "USER_ENTERED"
 
    .PARAMETER IncludeValuesInResponse
    Determines if the update response should include the values of the cells that were updated. By default, responses do not include the updated values
 
    .PARAMETER Launch
    If $true, opens the new SpreadSheet Url in your default browser
 
    .EXAMPLE
    $array | Export-GSSheet -NewSheetTitle "Finance Workbook" -Launch
 
 
    #>

    [OutputType('Google.Apis.Sheets.v4.Data.Spreadsheet')]
    [cmdletbinding(DefaultParameterSetName = "CreateNewSheetArray")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ParameterSetName = "UseExistingArray")]
        [parameter(Mandatory = $true,Position = 0,ParameterSetName = "UseExistingValue")]
        [String]
        $SpreadsheetId,
        [parameter(Mandatory = $false,Position = 0,ParameterSetName = "CreateNewSheetArray")]
        [parameter(Mandatory = $false,Position = 0,ParameterSetName = "CreateNewSheetValue")]
        [String]
        $NewSheetTitle,
        [parameter(Mandatory = $true,Position = 1,ValueFromPipeline = $true,ParameterSetName = "UseExistingArray")]
        [parameter(Mandatory = $true,Position = 1,ValueFromPipeline = $true,ParameterSetName = "CreateNewSheetArray")]
        [object[]]
        $Array,
        [parameter(Mandatory = $true,Position = 1,ParameterSetName = "UseExistingValue")]
        [parameter(Mandatory = $true,Position = 1,ParameterSetName = "CreateNewSheetValue")]
        [string]
        $Value,
        [parameter(Mandatory = $false)]
        [String]
        $SheetName,
        [parameter(Mandatory = $false,ParameterSetName = "UseExistingArray")]
        [parameter(Mandatory = $false,ParameterSetName = "CreateNewSheetArray")]
        [ValidateSet('Standard','Horizontal')]
        [String]
        $Style = "Standard",
        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('SpecifyRange')]
        [string]
        $Range,
        [parameter(Mandatory = $false)]
        [switch]
        $Append,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [ValidateSet("INPUT_VALUE_OPTION_UNSPECIFIED","RAW","USER_ENTERED")]
        [string]
        $ValueInputOption = "RAW",
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeValuesInResponse,
        [parameter(Mandatory = $false)]
        [Alias('Open')]
        [Switch]
        $Launch
    )
    Begin {
        $values = New-Object 'System.Collections.Generic.List[System.Collections.Generic.IList[Object]]'
    }
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Sheets.v4.SheetsService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($Value) {
                $finalArray = $([pscustomobject]@{Value = "$Value"})
                $Append = $true
            }
            else {
                if (!$contentType) {
                    $contentType = $Array[0].PSObject.TypeNames[0]
                }
                $finalArray = @()
                if ($contentType -eq 'System.String' -or $contentType -like "System.Int*") {
                    $Append = $true
                    foreach ($item in $Array) {
                        $finalArray += $([pscustomobject]@{Value = $item})
                    }
                }
                else {
                    foreach ($item in $Array) {
                        $finalArray += $item
                    }
                }
            }
            if (!$Append) {
                $propArray = New-Object 'System.Collections.Generic.List[Object]'
                $finalArray[0].PSObject.Properties.Name | ForEach-Object {
                    $propArray.Add($_)
                }
                $values.Add([System.Collections.Generic.IList[Object]]$propArray)
                $Append = $true
            }
            foreach ($object in $finalArray) {
                $valueArray = New-Object 'System.Collections.Generic.List[Object]'
                $object.PSobject.Properties.Value | ForEach-Object {
                    $valueArray.Add($_)
                }
                $values.Add([System.Collections.Generic.IList[Object]]$valueArray)
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
    End {
        try {
            if ($PSCmdlet.ParameterSetName -like "CreateNewSheet*") {
                if ($NewSheetTitle) {
                    Write-Verbose "Creating new spreadsheet titled: $NewSheetTitle"
                }
                else {
                    Write-Verbose "Creating new untitled spreadsheet"
                }
                $sheet = New-GSSheet -Title $NewSheetTitle -User $User -Verbose:$false
                $SpreadsheetId = $sheet.SpreadsheetId
                $SpreadsheetUrl = $sheet.SpreadsheetUrl
                $SheetName = 'Sheet1'
                Write-Verbose "New spreadsheet ID: $SpreadsheetId"
            }
            else {
                $sheet = Get-GSSheetInfo -SpreadsheetId $SpreadsheetId -User $User -IncludeGridData:$false -Verbose:$false
                $SpreadsheetUrl = $sheet.SpreadsheetUrl
            }
            if ($SheetName) {
                if ($Range -like "'*'!*") {
                    throw "SpecifyRange formatting error! When using the SheetName parameter, please exclude the SheetName when formatting the SpecifyRange value (i.e. 'A1:Z1000')"
                }
                elseif ($Range) {
                    $Range = "'$($SheetName)'!$Range"
                }
                else {
                    $Range = "$SheetName"
                }
            }
            $bodyData = (New-Object 'Google.Apis.Sheets.v4.Data.ValueRange' -Property @{
                Range = $Range
                MajorDimension = "$(if($Style -eq 'Horizontal'){'COLUMNS'}else{'ROWS'})"
                Values = [System.Collections.Generic.IList[System.Collections.Generic.IList[Object]]]$values
            })
            $body = New-Object 'Google.Apis.Sheets.v4.Data.BatchUpdateValuesRequest'
            $body.ValueInputOption = $ValueInputOption
            $body.IncludeValuesInResponse = $IncludeValuesInResponse
            $body.Data = [Google.Apis.Sheets.v4.Data.ValueRange[]]$bodyData
            $request = $service.Spreadsheets.Values.BatchUpdate($body,$SpreadsheetId)
            Write-Verbose "Updating Range '$Range' on Spreadsheet '$SpreadsheetId' for user '$User'"
            $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru | Add-Member -MemberType NoteProperty -Name 'SpreadsheetUrl' -Value $SpreadsheetUrl -PassThru
            if ($Launch) {
                Write-Verbose "Launching new spreadsheet at $SpreadsheetUrl"
                Start-Process $SpreadsheetUrl
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Export-GSSheet'
function Get-GSSheetInfo {
    <#
    .SYNOPSIS
    Gets metadata about a SpreadSheet
 
    .DESCRIPTION
    Gets metadata about a SpreadSheet
 
    .PARAMETER SpreadsheetId
    The unique Id of the SpreadSheet to retrieve info for
 
    .PARAMETER User
    The owner of the SpreadSheet
 
    .PARAMETER SheetName
    The name of the Sheet to retrieve info for
 
    .PARAMETER Range
    The specific range of the Sheet to retrieve info for
 
    .PARAMETER IncludeGridData
    Whether or not to include Grid Data in the response. Defaults to $false
 
    .PARAMETER Fields
    The fields to return in the response
 
    Available values are:
    * "namedRanges"
    * "properties"
    * "sheets"
    * "spreadsheetId"
 
    .PARAMETER Raw
    If $true, return the raw response, otherwise, return a flattened response for readability
 
    .EXAMPLE
    Get-GSSheetInfo -SpreadsheetId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976'
 
    Gets the info for the SpreadSheet provided
    #>

    [OutputType('Google.Apis.Sheets.v4.Data.Spreadsheet')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true)]
        [String]
        $SpreadsheetId,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [String]
        $SheetName,
        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('SpecifyRange')]
        [string]
        $Range,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeGridData,
        [parameter(Mandatory = $false)]
        [ValidateSet("namedRanges","properties","sheets","spreadsheetId")]
        [string[]]
        $Fields,
        [parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Sheets.v4.SheetsService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($SheetName) {
                if ($Range -like "'*'!*") {
                    throw "SpecifyRange formatting error! When using the SheetName parameter, please exclude the SheetName when formatting the SpecifyRange value (i.e. 'A1:Z1000')"
                }
                elseif ($Range) {
                    $Range = "'$($SheetName)'!$Range"
                }
                else {
                    $Range = "$SheetName"
                }
            }
            $request = $service.Spreadsheets.Get($SpreadsheetId)
            if ($Range) {
                $request.Ranges = [Google.Apis.Util.Repeatable[String]]::new([String[]]$Range)
            }
            if ($Fields) {
                $request.Fields = "$(($Fields | ForEach-Object {$f = $_;@("namedRanges","properties","sheets","spreadsheetId") | Where-Object {$_ -eq $f}}) -join ",")"
            }
            if ($PSBoundParameters.Keys -contains 'IncludeGridData') {
                $request.IncludeGridData = $IncludeGridData
            }
            Write-Verbose "Getting Spreadsheet Id '$SpreadsheetId' for user '$User'"
            $response = $request.Execute()
            if (!$Raw) {
                $response = $response | Select-Object @{N = "Title";E = {$_.properties.title}},@{N = "MaxRows";E = {[int]($_.sheets.properties.gridProperties.rowCount | Sort-Object | Select-Object -Last 1)}},@{N = "MaxColumns";E = {[int]($_.sheets.properties.gridProperties.columnCount | Sort-Object | Select-Object -Last 1)}},*
            }
            $response | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru

        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSSheetInfo'
function Import-GSSheet {
    <#
    .SYNOPSIS
    Imports data from a Sheet as if it was a CSV
 
    .DESCRIPTION
    Imports data from a Sheet as if it was a CSV
 
    .PARAMETER SpreadsheetId
    The unique Id of the SpreadSheet to import data from
 
    .PARAMETER SheetName
    The name of the Sheet to import data from
 
    .PARAMETER User
    The owner of the SpreadSheet
 
    .PARAMETER Range
    The specific range to import data from
 
    .PARAMETER RowStart
    The starting row of data. Useful if the headers for your table are not in Row 1 of the Sheet
 
    .PARAMETER Headers
    Allows you to define the headers for the rows on the sheet, in case there is no header row
 
    .PARAMETER DateTimeRenderOption
    How to render the DateTime cells
 
    Available values are:
    * "FORMATTED_STRING" (Default)
    * "SERIAL_NUMBER"
 
    .PARAMETER ValueRenderOption
    How to render the value cells and formula cells
 
    Available values are:
    * "FORMATTED_VALUE" (Default)
    * "UNFORMATTED_VALUE"
    * "FORMULA"
 
    .PARAMETER MajorDimension
    The major dimension that results should use.
 
    For example, if the spreadsheet data is: A1=1,B1=2,A2=3,B2=4, then requesting range=A1:B2,majorDimension=ROWS will return [[1,2],[3,4]], whereas requesting range=A1:B2,majorDimension=COLUMNS will return [[1,3],[2,4]].
 
    Available values are:
    * "ROWS" (Default)
    * "COLUMNS"
    * "DIMENSION_UNSPECIFIED"
 
    .PARAMETER As
    Whether to return the result set as an array of PSObjects or an array of DataRows
 
    Available values are:
    * "PSObject" (Default)
    * "DataRow"
 
    .PARAMETER Raw
    If $true, return the raw response, otherwise, return a flattened response for readability
 
    .EXAMPLE
    Import-GSSheet -SpreadsheetId '1rhsAYTOB_vrpvfwImPmWy0TcVa2sgmQa_9u976' -SheetName Sheet1 -RowStart 2 -Range 'B:C'
 
    Imports columns B-C as an Array of PSObjects, skipping the first row and treating Row 2 as the header row. Objects in the array will be what's contained in range 'B3:C' after that
    #>

    [cmdletbinding(DefaultParameterSetName = "Import")]
    Param
    (
        [parameter(Mandatory = $true)]
        [String]
        $SpreadsheetId,
        [parameter(Mandatory = $false)]
        [String]
        $SheetName,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('SpecifyRange')]
        [string]
        $Range,
        [parameter(Mandatory = $false,ParameterSetName = "Import")]
        [int]
        $RowStart = 1,
        [parameter(Mandatory = $false,ParameterSetName = "Import")]
        [string[]]
        $Headers,
        [parameter(Mandatory = $false)]
        [ValidateSet("FORMATTED_STRING","SERIAL_NUMBER")]
        [string]
        $DateTimeRenderOption = "FORMATTED_STRING",
        [parameter(Mandatory = $false)]
        [ValidateSet("FORMATTED_VALUE","UNFORMATTED_VALUE","FORMULA")]
        [string]
        $ValueRenderOption = "FORMATTED_VALUE",
        [parameter(Mandatory = $false)]
        [ValidateSet("ROWS","COLUMNS","DIMENSION_UNSPECIFIED")]
        [string]
        $MajorDimension = "ROWS",
        [Parameter(Mandatory = $false,ParameterSetName = "Import")]
        [ValidateSet("DataRow","PSObject")]
        [string]
        $As = "PSObject",
        [parameter(Mandatory = $false,ParameterSetName = "Raw")]
        [switch]
        $Raw
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Sheets.v4.SheetsService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            if ($SheetName) {
                if ($Range -like "'*'!*") {
                    throw "SpecifyRange formatting error! When using the SheetName parameter, please exclude the SheetName when formatting the SpecifyRange value (i.e. 'A1:Z1000')"
                }
                elseif ($Range) {
                    $Range = "'$($SheetName)'!$Range"
                }
                else {
                    $Range = "$SheetName"
                }
            }
            $request = $service.Spreadsheets.Values.BatchGet($SpreadsheetId)
            $request.Ranges = [Google.Apis.Util.Repeatable[String]]::new([String[]]$Range)
            $request.DateTimeRenderOption = [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+GetRequest+DateTimeRenderOptionEnum]::$($DateTimeRenderOption -replace "_","")
            $request.ValueRenderOption = [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+GetRequest+ValueRenderOptionEnum]::$($ValueRenderOption -replace "_","")
            $request.MajorDimension = [Google.Apis.Sheets.v4.SpreadsheetsResource+ValuesResource+GetRequest+MajorDimensionEnum]::$($MajorDimension -replace "_","")
            if ($MajorDimension -ne "ROWS" -and !$Raw) {
                $Raw = $true
                Write-Warning "Setting -Raw to True -- Parsing requires the MajorDimension to be set to ROWS (default value)"
            }
            Write-Verbose "Importing Range '$Range' from Spreadsheet '$SpreadsheetId' for user '$User'"
            $response = $request.Execute()
            if (!$Raw) {
                $i = 0
                $datatable = New-Object System.Data.Datatable
                if ($Headers) {
                    foreach ($col in $Headers) {
                        [void]$datatable.Columns.Add("$col")
                    }
                    $i++
                }
                $(if ($RowStart) {
                        $response.valueRanges.values | Select-Object -Skip $([int]$RowStart - 1)
                    }
                    else {
                        $response.valueRanges.values
                    }) | ForEach-Object {
                    if ($i -eq 0) {
                        foreach ($col in $_) {
                            [void]$datatable.Columns.Add("$col")
                        }
                    }
                    else {
                        [void]$datatable.Rows.Add([String[]]$_)
                    }
                    $i++
                }
                switch ($As) {
                    DataRow {
                        Write-Verbose "Created DataTable with $($i - 1) DataRows"
                        $datatable
                    }
                    PSObject {
                        Write-Verbose "Created PSObject array with $($i - 1) objects"
                        foreach ($row in $datatable) {
                            $obj = [Ordered]@{}
                            $props = $row.Table.Columns.ColumnName
                            foreach ($prop in $props) {
                                $obj[$prop] = $row.$prop
                            }
                            [PSCustomObject]$obj
                        }
                    }
                }
            }
            else {
                $response | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            }

        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Import-GSSheet'
function New-GSSheet {
    <#
    .SYNOPSIS
    Creates a new SpreadSheet
 
    .DESCRIPTION
    Creates a new SpreadSheet
 
    .PARAMETER Title
    The name of the new SpreadSheet
 
    .PARAMETER User
    The user to create the Sheet for
 
    .PARAMETER Launch
    If $true, opens the new SpreadSheet Url in your default browser
 
    .EXAMPLE
    New-GSSheet -Title "Finance Workbook" -Launch
 
    Creates a new SpreadSheet titled "Finance Workbook" and opens it in the browser on creation
    #>

    [OutputType('Google.Apis.Sheets.v4.Data.Spreadsheet')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false)]
        [Alias('SheetTitle')]
        [String]
        $Title,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [Alias('Open')]
        [Switch]
        $Launch
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Sheets.v4.SheetsService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Sheets.v4.Data.Spreadsheet'
            $body.Properties = New-Object 'Google.Apis.Sheets.v4.Data.SpreadsheetProperties' -Property @{
                Title = $Title
            }
            if (!$Title) {
                $Title = "Untitled spreadsheet"
            }
            Write-Verbose "Creating Spreadsheet '$Title' for user '$User'"
            $request = $service.Spreadsheets.Create($body)
            $response = $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            if ($Launch) {
                Write-Verbose "Launching new spreadsheet at $($response.SpreadsheetUrl)"
                Start-Process $response.SpreadsheetUrl
            }
            $response
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSSheet'
function Submit-GSSheetBatchUpdateRequest {
    <#
    .SYNOPSIS
    Submits a batch update request to a Google Sheet
 
    .DESCRIPTION
    Submits a batch update request to a Google Sheet
 
    .PARAMETER Title
    The name of the SpreadSheet
 
    .PARAMETER User
    The user to update the Sheet for
 
    .PARAMETER Launch
    If $true, opens the SpreadSheet Url in your default browser
 
    .EXAMPLE
    Update-GSSheet -Title "Finance Workbook" -Launch
 
    Creates a new SpreadSheet titled "Finance Workbook" and opens it in the browser on creation
    #>

    [OutputType('Google.Apis.Sheets.v4.Data.Spreadsheet')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias('SheetId')]
        [String]
        $Id,
        [parameter(Mandatory = $false)]
        [Alias('SheetTitle')]
        [String]
        $Title,
        [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)]
        [Alias('Owner','PrimaryEmail','UserKey','Mail')]
        [string]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [Alias('Open')]
        [Switch]
        $Launch
    )
    Process {
        if ($User -ceq 'me') {
            $User = $Script:PSGSuite.AdminEmail
        }
        elseif ($User -notlike "*@*.*") {
            $User = "$($User)@$($Script:PSGSuite.Domain)"
        }
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/drive'
            ServiceType = 'Google.Apis.Sheets.v4.SheetsService'
            User        = $User
        }
        $service = New-GoogleService @serviceParams
        try {
            $body = New-Object 'Google.Apis.Sheets.v4.Data.Spreadsheet'
            $body.Properties = New-Object 'Google.Apis.Sheets.v4.Data.SpreadsheetProperties' -Property @{
                Title = $Title
            }
            if (!$Title) {
                $Title = "Untitled spreadsheet"
            }
            Write-Verbose "Creating Spreadsheet '$Title' for user '$User'"
            $request = $service.Spreadsheets.Create($body)
            $response = $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -PassThru
            if ($Launch) {
                Write-Verbose "Launching new spreadsheet at $($response.SpreadsheetUrl)"
                Start-Process $response.SpreadsheetUrl
            }
            $response
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSSheet'
function Get-GSUser {
    <#
    .SYNOPSIS
    Gets the specified G SUite User or a list of Users
 
    .DESCRIPTION
    Gets the specified G SUite User. Designed for parity with Get-ADUser as much as possible
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to get info for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .PARAMETER Filter
    Query string for searching user fields
 
    For more information on constructing user queries, see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
 
    PowerShell filter syntax here is supported as "best effort". Please use Google's filter operators and syntax to ensure best results
 
    .PARAMETER Domain
    The specific domain you would like to list users for. Useful for customers with multiple domains.
 
    .PARAMETER SearchBase
    The organizational unit path that you would like to list users from
 
    .PARAMETER SearchScope
    The depth at which to return the list of users
 
    Available values are:
    * "Base": only return the users specified in the SearchBase
    * "Subtree": return the full list of users underneath the specified SearchBase
    * "OneLevel": return the SearchBase and the Users directly underneath it
 
    .PARAMETER ShowDeleted
    Returns deleted users
 
    .PARAMETER Projection
    What subset of fields to fetch for this user
 
    Acceptable values are:
    * "Basic": Do not include any custom fields for the user
    * "Custom": Include custom fields from schemas requested in customFieldMask
    * "Full": Include all fields associated with this user (default for this module)
 
    .PARAMETER CustomFieldMask
    A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when using '-Projection Custom'
 
    .PARAMETER ViewType
    Whether to fetch the administrator-only or domain-wide public view of the user. For more information, see Retrieve a user as a non-administrator
 
    Acceptable values are:
    * "Admin_View": Results include both administrator-only and domain-public fields for the user. (default)
    * "Domain_Public": Results only include fields for the user that are publicly visible to other users in the domain.
 
    .PARAMETER Fields
    The specific fields to fetch for this user
 
    .PARAMETER PageSize
    Page size of the result set
 
    .PARAMETER Limit
    The maximum amount of results you want returned. Exclude or set to 0 to return all results
 
    .PARAMETER OrderBy
    Property to use for sorting results.
 
    Acceptable values are:
    * "Email": Primary email of the user.
    * "FamilyName": User's family name.
    * "GivenName": User's given name.
 
    .PARAMETER SortOrder
    Whether to return results in ascending or descending order.
 
    Acceptable values are:
    * "Ascending": Ascending order.
    * "Descending": Descending order.
 
    .EXAMPLE
    Get-GSUser
 
    Gets the user info for the AdminEmail on the config
 
    .EXAMPLE
    Get-GSUser -Filter *
 
    Gets the list of users
 
    .EXAMPLE
    Get-GSUser -Filter "IsAdmin -eq '$true'"
 
    Gets the list of SuperAdmin users
 
    .EXAMPLE
    Get-GSUser -Filter "IsEnrolledIn2Sv -eq '$false'" -SearchBase /Contractors -SearchScope Subtree
 
    Gets the list of users not currently enrolled in 2-Step Verification from the Contractors OrgUnit or any OrgUnits underneath it
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.User')]
    [cmdletbinding(DefaultParameterSetName = "Get")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Get")]
        [Alias("PrimaryEmail","UserKey","Mail","Email","Id")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias("Query")]
        [String[]]
        $Filter = '*',
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [String]
        $Domain,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias("OrgUnitPath")]
        [String]
        $SearchBase,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateSet("Base","OneLevel","Subtree")]
        [String]
        $SearchScope = "Subtree",
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Switch]
        $ShowDeleted,
        [parameter(Mandatory = $false)]
        [ValidateSet("Basic","Custom","Full")]
        [string]
        $Projection = "Full",
        [parameter(Mandatory = $false)]
        [String]
        $CustomFieldMask,
        [parameter(Mandatory = $false)]
        [ValidateSet("Admin_View","Domain_Public")]
        [String]
        $ViewType = "Admin_View",
        [parameter(Mandatory = $false)]
        [String[]]
        $Fields,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateRange(1,500)]
        [Alias("MaxResults")]
        [Int]
        $PageSize = 500,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [Alias('First')]
        [Int]
        $Limit = 0,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateSet("Email","GivenName","FamilyName")]
        [String]
        $OrderBy,
        [parameter(Mandatory = $false,ParameterSetName = "List")]
        [ValidateSet("Ascending","Descending")]
        [String]
        $SortOrder
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.readonly'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        if ($MyInvocation.InvocationName -ne 'Get-GSUserList' -and $PSCmdlet.ParameterSetName -eq 'Get') {
            foreach ($U in $User) {
                try {
                    Resolve-Email ([ref]$U)
                    Write-Verbose "Getting User '$U'"
                    $request = $service.Users.Get($U)
                    $request.Projection = $Projection
                    $request.ViewType = ($ViewType -replace '_','')
                    if ($CustomFieldMask) {
                        $request.CustomFieldMask = $CustomFieldMask
                    }
                    if ($Fields) {
                        $request.Fields = "$($Fields -join ",")"
                    }
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -Force -PassThru  | Add-Member -MemberType ScriptMethod -Name ToString -Value {$this.PrimaryEmail} -PassThru -Force
                }
                catch {
                    if ($ErrorActionPreference -eq 'Stop') {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    else {
                        Write-Error $_
                    }
                }
            }
        }
        else {
            try {
                $request = $service.Users.List()
                $request.Projection = $Projection
                if ($PSBoundParameters.Keys -contains 'Domain') {
                    $verbScope = "domain '$($PSBoundParameters['Domain'])'"
                    $request.Domain = $PSBoundParameters['Domain']
                }
                elseif ($Script:PSGSuite.Preference) {
                    switch ($Script:PSGSuite.Preference) {
                        Domain {
                            $verbScope = "domain '$($Script:PSGSuite.Domain)'"
                            $request.Domain = $Script:PSGSuite.Domain
                        }
                        CustomerID {
                            $verbScope = "customer '$($Script:PSGSuite.CustomerID)'"
                            $request.Customer = "$($Script:PSGSuite.CustomerID)"
                        }
                    }
                }
                else {
                    $verbScope = "customer 'my_customer'"
                    $request.Customer = "my_customer"
                }
                if ($Limit -gt 0 -and $PageSize -gt $Limit) {
                    Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with first page" -f $PageSize,$Limit)
                    $PageSize = $Limit
                }
                $request.MaxResults = $PageSize
                foreach ($prop in $PSBoundParameters.Keys | Where-Object {$_ -in @('OrderBy','SortOrder','CustomFieldMask','ShowDeleted','ViewType')}) {
                    $request.$prop = $PSBoundParameters[$prop]
                }
                if (![String]::IsNullOrEmpty($Filter) -or $SearchBase) {
                    if ($Filter -eq '*') {
                        $Filter = ""
                    }
                    else {
                        $Filter = "$($Filter -join " ")"
                    }
                    if ($SearchBase) {
                        $Filter += " OrgUnitPath='$SearchBase'"
                    }
                    $Filter = $Filter -replace " -eq ","=" -replace " -like ",":" -replace " -match ",":" -replace " -contains ",":" -creplace "'True'","True" -creplace "'False'","False"
                    $request.Query = $Filter.Trim()
                    if ([String]::IsNullOrEmpty($Filter.Trim())) {
                        Write-Verbose "Getting all Users for $verbScope"
                    }
                    else {
                        Write-Verbose "Getting Users for $verbScope matching filter: `"$($Filter.Trim())`""
                    }
                }
                else {
                    Write-Verbose "Getting all Users for $verbScope"
                }
                $response = New-Object System.Collections.ArrayList
                [int]$i = 1
                $overLimit = $false
                do {
                    $result = $request.Execute()
                    if ($result.UsersValue) {
                        $result.UsersValue | ForEach-Object {
                            $_ | Add-Member -MemberType NoteProperty -Name 'User' -Value $_.PrimaryEmail -Force -PassThru | Add-Member -MemberType ScriptMethod -Name ToString -Value {$this.PrimaryEmail} -Force
                            [void]$response.Add($_)
                        }
                    }
                    $request.PageToken = $result.NextPageToken
                    [int]$retrieved = ($i + $result.UsersValue.Count) - 1
                    Write-Verbose "Retrieved $retrieved users..."
                    if ($Limit -gt 0 -and $retrieved -eq $Limit) {
                        Write-Verbose "Limit reached: $Limit"
                        $overLimit = $true
                    }
                    elseif ($Limit -gt 0 -and ($retrieved + $PageSize) -gt $Limit) {
                        $newPS = $Limit - $retrieved
                        Write-Verbose ("Reducing PageSize from {0} to {1} to meet limit with next page" -f $PageSize,$newPS)
                        $request.MaxResults = $newPS
                    }
                    [int]$i = $i + $result.UsersValue.Count
                }
                until ($overLimit -or !$result.NextPageToken)
                if ($SearchScope -ne "Subtree") {
                    if (!$SearchBase) {
                        $SearchBase = "/"
                    }
                    $response = switch ($SearchScope) {
                        Base {
                            $response | Where-Object {$_.OrgUnitPath -eq $SearchBase}
                        }
                        OneLevel {
                            $maxDepth = ($SearchBase -split "/" | Where-Object {$_}).Count + 1
                            $children = $response | Select-Object -ExpandProperty OrgUnitPath -Unique | ForEach-Object {
                                if (($_ -split "/" | Where-Object {$_}).Count -le $maxDepth) {
                                    $_
                                }
                            }
                            $response | Where-Object {$_.OrgUnitPath -in $children}
                        }
                    }
                    Write-Verbose "Total users in SearchScope: $($response.Count)"
                }
                return $response
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUser'
function Get-GSUserAlias {
    <#
    .SYNOPSIS
    Gets the specified G SUite User's aliases
 
    .DESCRIPTION
    Gets the specified G SUite User's aliases
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to get aliases for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .EXAMPLE
    Get-GSUserAlias
 
    Gets the list of aliases for the AdminEmail user
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Alias')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","Email")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.readonly'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($U in $User) {
            try {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Getting Alias list for User '$U'"
                $request = $service.Users.Aliases.List($U)
                $request.Execute() | Select-Object -ExpandProperty AliasesValue
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUserAlias'
function Get-GSUserPhoto {
    <#
    .SYNOPSIS
    Gets the photo data for the specified user
 
    .DESCRIPTION
    Gets the photo data for the specified user
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to get info for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    Defaults to the AdminEmail in the config
 
    .PARAMETER OutFilePath
    The directory path that you would like to save the photos to. If excluded, this will return the photo information
 
    .PARAMETER OutFileFormat
    The format that you would like to save the photo as.
 
    Available values are:
    * "PNG": saves the photo in .png format
    * "JPG": saves the photo in .jpg format
    * "Base64": saves the photo as a .txt file containing standard (non-WebSafe) Base64 content.
 
    Defaults to PNG
 
    .EXAMPLE
    Get-GSUserPhoto -OutFilePath .
 
    Saves the Google user photo of the AdminEmail in the current working directory as a .png image
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserPhoto')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Get")]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User = $Script:PSGSuite.AdminEmail,
        [parameter(Mandatory = $false)]
        [ValidateScript({(Get-Item $_).PSIsContainer})]
        [String]
        $OutFilePath,
        [parameter(Mandatory = $false)]
        [ValidateSet('Base64','PNG','JPG')]
        [String]
        $OutFileFormat = 'PNG'
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user.readonly'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            foreach ($U in $User) {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Getting photo for User '$U'"
                $request = $service.Users.Photos.Get($U)
                $res = $request.Execute()
                $base64 = $res.PhotoData | Convert-Base64 -From WebSafeBase64String -To Base64String
                $bytes = [Convert]::FromBase64String($base64)
                if ($OutFilePath) {
                    $fileBaseName = "$($U -replace '@.*','')"
                    switch ($OutFileFormat) {
                        JPG {
                            $filePath = Join-Path $OutFilePath "$($fileBaseName).jpg"
                            Write-Verbose "Saving photo at '$filePath'"
                            [System.IO.File]::WriteAllBytes($filePath, $bytes)
                        }
                        PNG {
                            $filePath = Join-Path $OutFilePath "$($fileBaseName).png"
                            Write-Verbose "Saving photo at '$filePath'"
                            [System.IO.File]::WriteAllBytes($filePath, $bytes)
                        }
                        Base64 {
                            $filePath = Join-Path $OutFilePath "$($fileBaseName).txt"
                            Write-Verbose "Saving Base64 photo content at '$filePath'"
                            [System.IO.File]::WriteAllText($filePath,$base64)
                        }
                    }
                }
                else {
                    $res | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru | Add-Member -MemberType NoteProperty -Name 'PhotoBytes' -Value $bytes -PassThru | Add-Member -MemberType NoteProperty -Name 'PhotoBase64' -Value $base64 -PassThru
                }
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Get-GSUserPhoto'
function Invoke-GSUserOffboarding {
    <#
    .SYNOPSIS
    Wraps some common offboarding tasks such as random password setting, OAuth token revocation, mobile device removal, and more.
 
    .DESCRIPTION
    Wraps some common offboarding tasks such as random password setting, OAuth token revocation, mobile device removal, and more.
 
    This function outputs in a log-style, timestamped format that is intended for auditability.
 
    .PARAMETER User
    The User to offboard
 
    .PARAMETER Options
    The tasks you would like to perform on the User. Defaults to the following: 'ClearASPs','ClearOAuthTokens','RemoveMobileDevices','Suspend','SetRandomPassword'
 
    Available options:
    * 'Full' - Performs all of the below tasks
    * 'ClearASPs' - Clears Application Specific Passwords
    * 'ClearOAuthTokens' - Clears OAuth tokens
    * 'RemoveMobileDevices' - Removes Mobile Devices
    * 'Suspend' - Suspends the user account
    * 'SetRandomPassword' - Sets the user's account to a random password
    * 'MoveToOrgUnit' - Moves the user to the DestinationOrgUnit specified
    * 'SetLicense' - Sets the user to a different license
 
    .PARAMETER DestinationOrgUnit
    If Options include Full or MoveToOrgUnit, this is the OrgUnit that the user will be moved to.
 
    .PARAMETER License
    The License to set the user to.
 
    .EXAMPLE
    Invoke-GSUserOffboarding -User tom.fields@domain.com -Options Full -DestinationOrgUnit '/Former Employees'
 
    Performs all of the listed tasks against user Tom Fields, including moving them to the '/Former Employees' OrgUnit and setting them to a VFE license.
 
    .NOTES
    Pull requests welcome for functionality enhancements!
    #>

    [OutputType('System.String')]
    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param(
        [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('PrimaryEmail','Mail')]
        [string[]]
        $User,
        [Parameter()]
        [ValidateSet('Full','ClearASPs','ClearOAuthTokens','RemoveMobileDevices','Suspend','SetRandomPassword','MoveToOrgUnit','SetLicense')]
        [String[]]
        $Options = @('ClearASPs','ClearOAuthTokens','RemoveMobileDevices','Suspend','SetRandomPassword'),
        [Parameter()]
        [string]
        $DestinationOrgUnit,
        [Parameter()]
        [ValidateSet("G-Suite-Enterprise","Google-Apps-Unlimited","Google-Apps-For-Business","Google-Apps-For-Postini","Google-Apps-Lite","Google-Drive-storage-20GB","Google-Drive-storage-50GB","Google-Drive-storage-200GB","Google-Drive-storage-400GB","Google-Drive-storage-1TB","Google-Drive-storage-2TB","Google-Drive-storage-4TB","Google-Drive-storage-8TB","Google-Drive-storage-16TB","Google-Vault","Google-Vault-Former-Employee","1010020020")]
        [string]
        $License = "Google-Vault-Former-Employee"
    )
    Begin {
        function New-RandomPassword {
            Param (
                [parameter(Mandatory = $false)]
                [int]
                $Length = 15
            )
            $ascii = $null
            for ($a = 33;$a -le 126;$a++) {
                $ascii += ,[char][byte]$a
            }
            for ($loop = 1; $loop -le $length; $loop++) {
                $randomPassword += ($ascii | Get-Random)
            }
            return ([String]$randomPassword)
        }
    }
    Process {
        foreach ($U in $User) {
            Resolve-Email ([ref]$U)
            if ($PSCmdlet.ShouldProcess("Offboarding user: $U")) {
                Write-Verbose "Offboarding user: $U"
                "[$(Get-Date -Format o)] Starting offboard of user: $U"
                $_user = @{User = $U}
                $updateParams = @{Confirm = $false}
                foreach ($opt in $options) {
                    switch -RegEx ($opt) {
                        '(Full|Suspend)' {
                            $updateParams['Suspended'] = $true
                        }
                        '(Full|SetRandomPassword)' {
                            $updateParams['Password'] = ConvertTo-SecureString (New-RandomPassword) -AsPlainText -Force
                            $updateParams['ChangePasswordAtNextLogin'] = $true
                        }
                        '(Full|MoveToOrgUnit)' {
                            if ($PSBoundParameters.ContainsKey('DestinationOrgUnit')) {
                                $updateParams['OrgUnitPath'] = $PSBoundParameters['DestinationOrgUnit']
                            }
                            else {
                                throw "No DestinationOrgUnit provided!! Stopping further processing"
                                exit 1
                            }
                        }
                    }
                }
                "[$(Get-Date -Format o)] [$U] Updating user"
                Update-GSUser @_user @updateParams | Format-List PrimaryEmail,@{N = "FullName";E = {$_.name.fullName}},Suspended,ChangePasswordAtNextLogin,OrgUnitPath
                if ($Options -contains 'Full' -or $Options -contains 'ClearASPs') {
                    "[$(Get-Date -Format o)] [$U] Retrieving App Specific Passwords to remove"
                    $ASPs = Get-GSUserASPList @_user
                    if ($ASPs) {
                        foreach ($ASP in $ASPs) {
                            "[$(Get-Date -Format o)] [$U] Revoking ASP for '$($ASP.name)'"
                            Remove-GSUserASP @_user -CodeID $ASP.codeId -Confirm:$false
                        }
                        Remove-Variable ASPs -ErrorAction SilentlyContinue
                    }
                    else {
                        "[$(Get-Date -Format o)] [$U] User has no ASP's to remove!"
                    }
                }
                if ($Options -contains 'Full' -or $Options -contains 'ClearOAuthTokens') {
                    "[$(Get-Date -Format o)] [$U] Retrieving OAuth Tokens to remove"
                    $Tokens = Get-GSUserTokenList @user
                    if ($Tokens.clientId) {
                        foreach ($Token in $Tokens) {
                            "[$(Get-Date -Format o)] [$U] Revoking OAuth Token for '$($Token.displayText)'"
                            Remove-GSUserToken @user -ClientID $Token.clientId -Confirm:$false
                        }
                        Remove-Variable Tokens -ErrorAction SilentlyContinue
                    }
                    else {
                        "[$(Get-Date -Format o)] [$U] User has no OAuth Tokens to remove!"
                    }
                }
                if ($Options -contains 'Full' -or $Options -contains 'RemoveMobileDevices') {
                    "[$(Get-Date -Format o)] [$U] Retrieving Mobile Devices to remove"
                    $Mobiles = Get-GSMobileDeviceList @user -Projection BASIC
                    if ($Mobiles) {
                        foreach ($Mobile in $Mobiles) {
                            "[$(Get-Date -Format o)] [$U] Removing Mobile Device '$($Mobile.model)'"
                            Remove-GSMobileDevice -ResourceID $Mobile.resourceId -Confirm:$false
                        }
                        Remove-Variable Mobiles -ErrorAction SilentlyContinue
                    }
                    else {
                        "[$(Get-Date -Format o)] [$U] User has no Mobile Devices to remove!"
                    }
                }
                if ($Options -contains 'Full' -or $Options -contains 'SetLicense') {
                    if ($null -ne $License) {
                        "[$(Get-Date -Format o)] [$U] Setting user license to: $License"
                        Set-GSUserLicense @user -License $License | Format-List UserId,ProductId,SkuId
                    }
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Invoke-GSUserOffboarding'
function New-GSUser {
    <#
    .SYNOPSIS
    Creates a new G Suite user
 
    .DESCRIPTION
    Creates a new G Suite user
 
    .PARAMETER PrimaryEmail
    The primary email for the user. If a user with the desired email already exists, a GoogleApiException will be thrown
 
    .PARAMETER GivenName
    The given (first) name of the user
 
    .PARAMETER FamilyName
    The family (last) name of the user
 
    .PARAMETER FullName
    The full name of the user, if different from "$FirstName $LastName"
 
    .PARAMETER Password
    The password for the user. Requires a SecureString
 
    .PARAMETER ChangePasswordAtNextLogin
    If set, user will need to change their password on their first login
 
    .PARAMETER OrgUnitPath
    The OrgUnitPath to create the user in
 
    .PARAMETER Suspended
    If set, user will be created in a suspended state
 
    .PARAMETER Addresses
    The address objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserAddress[]' object type. You can create objects of this type easily by using the function 'Add-GSUserAddress'
 
    .PARAMETER Emails
    The email objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserEmail[]' object type. You can create objects of this type easily by using the function 'Add-GSUserEmail'
 
    .PARAMETER ExternalIds
    The externalId objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId[]' object type. You can create objects of this type easily by using the function 'Add-GSUserExternalId'
 
    .PARAMETER Ims
    The IM objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserIm[]' object type. You can create objects of this type easily by using the function 'Add-GSUserIm'
 
    .PARAMETER Locations
    The Location objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserLocation[]' object type. You can create objects of this type easily by using the function 'Add-GSUserLocation'
 
    .PARAMETER Organizations
    The organization objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization[]' object type. You can create objects of this type easily by using the function 'Add-GSUserOrganization'
 
    .PARAMETER Relations
    The relation objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserRelation[]' object type. You can create objects of this type easily by using the function 'Add-GSUserRelation'
 
    .PARAMETER Phones
    The phone objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserPhone[]' object type. You can create objects of this type easily by using the function 'Add-GSUserPhone'
 
    .PARAMETER IncludeInGlobalAddressList
    Indicates if the user's profile is visible in the G Suite global address list when the contact sharing feature is enabled for the domain. For more information about excluding user profiles, see the administration help center: http://support.google.com/a/bin/answer.py?answer=1285988
 
    .PARAMETER IpWhitelisted
    If true, the user's IP address is white listed: http://support.google.com/a/bin/answer.py?answer=60752
 
    .PARAMETER CustomSchemas
    Custom user attribute values to add to the user's account.
 
    The Custom Schema and it's fields **MUST** exist prior to updating these values for a user otherwise it will return an error.
 
    This parameter only accepts a hashtable where the keys are Schema Names and the value for each key is another hashtable, i.e.:
 
        Update-GSUser -User john.smith@domain.com -CustomSchemas @{
            schemaName1 = @{
                fieldName1 = $fieldValue1
                fieldName2 = $fieldValue2
            }
            schemaName2 = @{
                fieldName3 = $fieldValue3
            }
        }
 
    If you need to CLEAR a custom schema value, simply pass $null as the value(s) for the fieldName in the hashtable, i.e.:
 
        Update-GSUser -User john.smith@domain.com -CustomSchemas @{
            schemaName1 = @{
                fieldName1 = $null
                fieldName2 = $null
            }
            schemaName2 = @{
                fieldName3 = $null
            }
        }
 
    .EXAMPLE
    $address = Add-GSUserAddress -Country USA -Locality Dallas -PostalCode 75000 Region TX -StreetAddress '123 South St' -Type Work -Primary
 
    $phone = Add-GSUserPhone -Type Work -Value "(800) 873-0923" -Primary
 
    $extId = Add-GSUserExternalId -Type Login_Id -Value jsmith2
 
    $email = Add-GSUserEmail -Type work -Address jsmith@contoso.com
 
    New-GSUser -PrimaryEmail john.smith@domain.com -GivenName John -FamilyName Smith -Password (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) -ChangePasswordAtNextLogin -OrgUnitPath "/Users/New Hires" -IncludeInGlobalAddressList -Addresses $address -Phones $phone -ExternalIds $extId -Emails $email
 
    Creates a user named John Smith and adds their work address, work phone, login_id and alternate non gsuite work email to the user object.
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.User')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true, Position = 0)]
        [String]
        $PrimaryEmail,
        [parameter(Mandatory = $true)]
        [String]
        $GivenName,
        [parameter(Mandatory = $true)]
        [String]
        $FamilyName,
        [parameter(Mandatory = $false)]
        [String]
        $FullName,
        [parameter(Mandatory = $true)]
        [SecureString]
        $Password,
        [parameter(Mandatory = $false)]
        [Switch]
        $ChangePasswordAtNextLogin,
        [parameter(Mandatory = $false)]
        [String]
        $OrgUnitPath,
        [parameter(Mandatory = $false)]
        [Switch]
        $Suspended,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserAddress[]]
        $Addresses,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserEmail[]]
        $Emails,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId[]]
        $ExternalIds,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserIm[]]
        $Ims,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserLocation[]]
        $Locations,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization[]]
        $Organizations,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserRelation[]]
        $Relations,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserPhone[]]
        $Phones,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeInGlobalAddressList,
        [parameter(Mandatory = $false)]
        [Switch]
        $IpWhitelisted,
        [parameter(Mandatory = $false)]
        [ValidateScript( {
                $hash = $_
                foreach ($schemaName in $hash.Keys) {
                    if ($hash[$schemaName].GetType().Name -ne 'Hashtable') {
                        throw "The CustomSchemas parameter only accepts a hashtable where the value of the top-level keys must also be a hashtable. The key '$schemaName' has a value of type '$($hash[$schemaName].GetType().Name)'"
                        $valid = $false
                    }
                    else {
                        $valid = $true
                    }
                }
                $valid
            })]
        [Hashtable]
        $CustomSchemas
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        try {
            Write-Verbose "Creating user '$PrimaryEmail'"
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.User'
            $name = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserName' -Property @{
                GivenName  = $GivenName
                FamilyName = $FamilyName
            }
            if ($PSBoundParameters.Keys -contains 'FullName') {
                $name.FullName = $FullName
            }
            else {
                $name.FullName = "$GivenName $FamilyName"
            }
            $body.Name = $name
            foreach ($prop in $PSBoundParameters.Keys | Where-Object {$body.PSObject.Properties.Name -contains $_}) {
                switch ($prop) {
                    PrimaryEmail {
                        if ($PSBoundParameters[$prop] -notlike "*@*.*") {
                            $PSBoundParameters[$prop] = "$($PSBoundParameters[$prop])@$($Script:PSGSuite.Domain)"
                        }
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                    Password {
                        $body.Password = (New-Object PSCredential "user", $Password).GetNetworkCredential().Password
                    }
                    Emails {
                        $emailList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserEmail]'
                        foreach ($email in $Emails) {
                            $emailList.Add($email)
                        }
                        $body.Emails = $emailList
                    }
                    ExternalIds {
                        $extIdList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId]'
                        foreach ($extId in $ExternalIds) {
                            $extIdList.Add($extId)
                        }
                        $body.ExternalIds = $extIdList
                    }
                    Ims {
                        $imList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserIm]'
                        foreach ($im in $Ims) {
                            $imList.Add($im)
                        }
                        $body.Ims = $imList
                    }
                    Locations {
                        $locationList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserLocation]'
                        foreach ($loc in $Locations) {
                            $locationList.Add($loc)
                        }
                        $body.Locations = $locationList
                    }
                    Organizations {
                        $orgList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization]'
                        foreach ($organization in $Organizations) {
                            $orgList.Add($organization)
                        }
                        $body.Organizations = $orgList
                    }
                    Relations {
                        $relList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserRelation]'
                        foreach ($relation in $Relations) {
                            $relList.Add($relation)
                        }
                        $body.Relations = $relList
                    }
                    Phones {
                        $phoneList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserPhone]'
                        foreach ($phone in $Phones) {
                            $phoneList.Add($phone)
                        }
                        $body.Phones = $phoneList
                    }
                    CustomSchemas {
                        $schemaDict = New-Object 'System.Collections.Generic.Dictionary`2[[System.String],[System.Collections.Generic.IDictionary`2[[System.String],[System.Object]]]]'
                        foreach ($schemaName in $CustomSchemas.Keys) {
                            $fieldDict = New-Object 'System.Collections.Generic.Dictionary`2[[System.String],[System.Object]]'
                            $schemaFields = $CustomSchemas[$schemaName]
                            $schemaFields.Keys | ForEach-Object {
                                $fieldDict.Add($_, $schemaFields[$_])
                            }
                            $schemaDict.Add($schemaName, $fieldDict)
                        }
                        $body.CustomSchemas = $schemaDict
                    }
                    Default {
                        $body.$prop = $PSBoundParameters[$prop]
                    }
                }
            }
            $request = $service.Users.Insert($body)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSUser'
function New-GSUserAlias {
    <#
    .SYNOPSIS
    Creates a new alias for a G Suite user
 
    .DESCRIPTION
    Creates a new alias for a G Suite user
 
    .PARAMETER User
    The user to create the alias for
 
    .PARAMETER Alias
    The alias or list of aliases to create for the user
 
    .EXAMPLE
    New-GSUserAlias -User john.smith@domain.com -Alias 'jsmith@domain.com','johns@domain.com'
 
    Creates 2 new aliases for user John Smith as 'jsmith@domain.com' and 'johns@domain.com'
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.Alias')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","Email")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory = $true,Position = 1)]
        [String[]]
        $Alias
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($A in $Alias) {
            try {
                if ($User -ceq 'me') {
                    $User = $Script:PSGSuite.AdminEmail
                }
                elseif ($User -notlike "*@*.*") {
                    $User = "$($User)@$($Script:PSGSuite.Domain)"
                }
                if ($A -notlike "*@*.*") {
                    $A = "$($A)@$($Script:PSGSuite.Domain)"
                }
                Write-Verbose "Creating alias '$A' for user '$User'"
                $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.Alias'
                $body.AliasValue = $A
                $request = $service.Users.Aliases.Insert($body,$User)
                $request.Execute()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'New-GSUserAlias'
function Remove-GSUser {
    <#
    .SYNOPSIS
    Removes a user
     
    .DESCRIPTION
    Removes a user
     
    .PARAMETER User
    The primary email or unique Id of the user to Remove-GSUser
     
    .EXAMPLE
    Remove-GSUser joe -Confirm:$false
 
    Removes the user 'joe@domain.com', skipping confirmation
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $false,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($U in $User) {
            try {
                if ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                if ($PSCmdlet.ShouldProcess("Deleting user '$U'")) {
                    Write-Verbose "Deleting user '$U'"
                    $request = $service.Users.Delete($U)
                    $request.Execute()
                    Write-Verbose "User '$U' has been successfully deleted"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSUser'
function Remove-GSUserAlias {
    <#
    .SYNOPSIS
    Removes an alias from a G Suite user
     
    .DESCRIPTION
    Removes an alias from a G Suite user
     
    .PARAMETER User
    The user to remove the alias from
     
    .PARAMETER Alias
    The alias or list of aliases to remove from the user
     
    .EXAMPLE
    Remove-GSUserAlias -User john.smith@domain.com -Alias 'jsmith@domain.com','johns@domain.com'
 
    Removes 2 aliases from user John Smith: 'jsmith@domain.com' and 'johns@domain.com'
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail","Email")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory = $true,Position = 1)]
        [String[]]
        $Alias
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($A in $Alias) {
            try {
                if ($User -ceq 'me') {
                    $User = $Script:PSGSuite.AdminEmail
                }
                elseif ($User -notlike "*@*.*") {
                    $User = "$($User)@$($Script:PSGSuite.Domain)"
                }
                if ($A -notlike "*@*.*") {
                    $A = "$($A)@$($Script:PSGSuite.Domain)"
                }
                if ($PSCmdlet.ShouldProcess("Removing alias '$A' from user '$User'")) {
                    Write-Verbose "Removing alias '$A' from user '$User'"
                    $request = $service.Users.Aliases.Delete($User,$A)
                    $request.Execute()
                    Write-Verbose "Alias '$A' has been successfully deleted from user '$User'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSUserAlias'
function Remove-GSUserPhoto {
    <#
    .SYNOPSIS
    Removes the photo for the specified user
 
    .DESCRIPTION
    Removes the photo for the specified user
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to remove the photo for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    .EXAMPLE
    Remove-GSUserPhoto -User me
 
    Removes the Google user photo of the AdminEmail user
    #>

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "High")]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($U in $User) {
            try {
                if ($U -ceq 'me') {
                    $U = $Script:PSGSuite.AdminEmail
                }
                elseif ($U -notlike "*@*.*") {
                    $U = "$($U)@$($Script:PSGSuite.Domain)"
                }
                if ($PSCmdlet.ShouldProcess("Removing the photo for User '$U'")) {
                    Write-Verbose "Removing the photo for User '$U'"
                    $request = $service.Users.Photos.Delete($U)
                    $request.Execute()
                    Write-Verbose "Successfully removed the photo for user '$U'"
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}
Export-ModuleMember -Function 'Remove-GSUserPhoto'
function Restore-GSUser {
    <#
    .SYNOPSIS
    Restores a deleted user
 
    .DESCRIPTION
    Restores a deleted user
 
    .PARAMETER User
    The email address of the user to restore
 
    .PARAMETER Id
    The unique Id of the user to restore
 
    .PARAMETER OrgUnitPath
    The OrgUnitPath to restore the user to
 
    Defaults to the root OrgUnit "/"
 
    .PARAMETER RecentOnly
    If multiple users with the email address are found in deleted users, this forces restoration of the most recently deleted user. If not passed and multiple deleted users are found with the specified email address, you will be prompted to choose which you'd like to restore based on deletion time
 
    .EXAMPLE
    Restore-GSUser -User john.smith@domain.com -OrgUnitPath "/Users/Rehires" -Confirm:$false
 
    Restores user John Smith to the OrgUnitPath "/Users/Rehires", skipping confirmation. If multiple accounts with the email "john.smith@domain.com" are found, the user is presented with a dialog to choose which account to restore based on deletion time
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.User')]
    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact = "Medium",DefaultParameterSetName = 'User')]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = 'User')]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User,
        [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = 'Id')]
        [Int]
        $Id,
        [parameter(Mandatory = $false,Position = 1)]
        [String]
        $OrgUnitPath = "/",
        [parameter(Mandatory = $false)]
        [Switch]
        $RecentOnly
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        if (!$User) {
            $User =  ($Id | ForEach-Object {"$_"})
            $GetId = $false
        }
    }
    Process {
        try {
            foreach ($U in $User) {
                $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserUndelete'
                $body.OrgUnitPath = $OrgUnitPath
                if (!$Id) {
                    if ($PSCmdlet.ShouldProcess("Undeleting user '$U'")) {
                        if ($U -notlike "*@*.*") {
                            $U = "$($U)@$($Script:PSGSuite.Domain)"
                        }
                        $delUsers = Get-GSUser -Filter "email=$U" -ShowDeleted -Verbose:$false | Where-Object {$_.PrimaryEmail -eq $U} | Sort-Object DeletionTime -Descending
                        $userId = if ($delUsers.Count -gt 1 -and !$RecentOnly) {
                            $i = 0
                            $options = @()
                            $idHash = @{}
                            $delUsers | ForEach-Object {
                                $i++
                                $optText = "$($i): $($_.DeletionTime.ToString())"
                                $options += @{"&$($optText)" = "User '$($_.PrimaryEmail)' deleted on $($_.DeletionTime.ToLongDateString()) at $($_.DeletionTime.ToLongTimeString())"}
                                $idHash[$optText] = $_.Id
                            }
                            $choice = Read-Prompt -Options $options -Title "`n** Choose Which User To Undelete **`n" -Message "There are $($delUsers.Count) deleted users with the email address '$U'. Please enter the number for the user you would like to undelete based on the time the account was deleted`n"
                            $idHash[$choice]
                        }
                        else {
                            $delUsers[0].Id
                        }
                    }
                }
                else {
                    $userId = $U
                }
                Write-Verbose "Undeleting User Id '$userId' [$U]"
                $request = $service.Users.Undelete($body,$userId)
                $request.Execute()
                Write-Verbose "User '$U' has been successfully undeleted"
            }
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Restore-GSUser'
function Sync-GSUserCache {
    <#
    .SYNOPSIS
    Syncs your GS Users to a hashtable contained in the global scoped variable $global:GSUserCache for fast lookups in scripts.
 
    .DESCRIPTION
    Syncs your GS Users to a hashtable contained in the global scoped variable $global:GSUserCache for fast lookups in scripts.
 
    .PARAMETER Filter
    The filter to use with Get-GSUser to populate your UserCache with.
 
    Defaults to * (all users).
 
    If you'd like to limit to just Active (not suspended) users, use the following filter:
 
        "IsSuspended -eq '$false'"
 
    .PARAMETER Keys
    The user properties to use as keys in the Cache hash.
 
    Available values are:
    * PrimaryEmail
    * Id
    * Alias
 
    Defaults to all 3.
 
    .PARAMETER PassThru
    If $true, returns the hashtable as output
 
    .EXAMPLE
    Sync-GSUserCache -Filter 'IsSuspended=False'
 
    Fills the $global:GSUserCache hashtable with all active users using the default Keys.
    #>

    [CmdLetBinding()]
    Param (
        [parameter(Mandatory = $false, Position = 0)]
        [String[]]
        $Filter = @('*'),
        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('PrimaryEmail','Id','Alias')]
        [String[]]
        $Keys = @('PrimaryEmail','Id','Alias'),
        [parameter(Mandatory = $false)]
        [Switch]
        $PassThru
    )
    Begin {
        $global:GSUserCache = @{}
    }
    Process {
        Write-Verbose "Syncing users to `$global:GSUserCache"
        Get-GSUser -Filter $Filter | ForEach-Object {
            if ($Keys -contains 'Id') {
                $global:GSUserCache[$_.Id] = $_
            }
            if ($Keys -contains 'PrimaryEmail') {
                $global:GSUserCache[$_.PrimaryEmail] = $_
            }
            if ($Keys -contains 'Alias') {
                foreach ($email in $_.Emails.Address) {
                    if (-not ($global:GSUserCache.ContainsKey($email))) {
                        $global:GSUserCache[$email] = $_
                    }
                }
            }
        }
    }
    End {
        if ($PassThru) {
            return $global:GSUserCache
        }
    }
}

Export-ModuleMember -Function 'Sync-GSUserCache'
function Update-GSUser {
    <#
    .SYNOPSIS
    Updates a user
 
    .DESCRIPTION
    Updates a user
 
    .PARAMETER User
    The primary email or unique Id of the user to update
 
    .PARAMETER PrimaryEmail
    The new primary email for the user. The previous primary email will become an alias automatically
 
    .PARAMETER GivenName
    The new given (first) name for the user
 
    .PARAMETER FamilyName
    The new family (last) name for the user
 
    .PARAMETER FullName
    The new full name for the user
 
    .PARAMETER Password
    The new password for the user as a SecureString
 
    .PARAMETER ChangePasswordAtNextLogin
    If set, user will need to change their password on their next login
 
    .PARAMETER OrgUnitPath
    The new OrgUnitPath for the user
 
    .PARAMETER Suspended
    If set to $true or passed as a bare switch (-Suspended), user will be suspended. If set to $false, user will be unsuspended. If excluded, user's suspension status will remain as-is
 
    .PARAMETER Addresses
    The address objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserAddress[]' object type. You can create objects of this type easily by using the function 'Add-GSUserAddress'
 
    .PARAMETER Emails
    The email objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserEmail[]' object type. You can create objects of this type easily by using the function 'Add-GSUserEmail'
 
    .PARAMETER ExternalIds
    The externalId objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId[]' object type. You can create objects of this type easily by using the function 'Add-GSUserExternalId'
 
    To CLEAR all values for a user, pass `$null` as the value for this parameter.
 
    .PARAMETER Ims
    The IM objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserIm[]' object type. You can create objects of this type easily by using the function 'Add-GSUserIm'
 
    To CLEAR all values for a user, pass `$null` as the value for this parameter.
 
    .PARAMETER Locations
    The Location objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserLocation[]' object type. You can create objects of this type easily by using the function 'Add-GSUserLocation'
 
    To CLEAR all values for a user, pass `$null` as the value for this parameter.
 
    .PARAMETER Organizations
    The organization objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization[]' object type. You can create objects of this type easily by using the function 'Add-GSUserOrganization'
 
    To CLEAR all values for a user, pass `$null` as the value for this parameter.
 
    .PARAMETER RecoveryEmail
    Recovery email of the user.
 
    .PARAMETER RecoveryPhone
    Recovery phone of the user. The phone number must be in the E.164 format, starting with the plus sign (+). Example: +16506661212. The value provided for RecoveryPhone is stripped of all non-digit characters and prepended with a + to ensure correct formatting.
 
    .PARAMETER Relations
    A list of the user's relationships to other users.
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserRelation[]' object type. You can create objects of this type easily by using the function 'Add-GSUserRelation'
 
    To CLEAR all values for a user, pass `$null` as the value for this parameter.
 
    .PARAMETER Phones
    The phone objects of the user
 
    This parameter expects a 'Google.Apis.Admin.Directory.directory_v1.Data.UserPhone[]' object type. You can create objects of this type easily by using the function 'Add-GSUserPhone'
 
    To CLEAR all values for a user, pass `$null` as the value for this parameter.
 
    .PARAMETER IncludeInGlobalAddressList
    Indicates if the user's profile is visible in the G Suite global address list when the contact sharing feature is enabled for the domain. For more information about excluding user profiles, see the administration help center: http://support.google.com/a/bin/answer.py?answer=1285988
 
    .PARAMETER IpWhitelisted
    If true, the user's IP address is white listed: http://support.google.com/a/bin/answer.py?answer=60752
 
    .PARAMETER IsAdmin
    If true, the user will be made a SuperAdmin. If $false, the user will have SuperAdmin privileges revoked.
 
    Requires confirmation.
 
    .PARAMETER Archived
    If true, the user will be assigned an Archived User license. If you do not have sufficient Archived User licenses, you will receive a 500 error with reason of "INSUFFICIENT_ARCHIVED_USER_LICENSES".
 
    .PARAMETER CustomSchemas
    Custom user attribute values to add to the user's account. This parameter only accepts a hashtable where the keys are Schema Names and the value for each key is another hashtable, i.e.:
 
        Update-GSUser -User john.smith@domain.com -CustomSchemas @{
            schemaName1 = @{
                fieldName1 = $fieldValue1
                fieldName2 = $fieldValue2
            }
            schemaName2 = @{
                fieldName3 = $fieldValue3
            }
        }
 
    If you need to CLEAR a custom schema value, simply pass $null as the value(s) for the fieldName in the hashtable, i.e.:
 
        Update-GSUser -User john.smith@domain.com -CustomSchemas @{
            schemaName1 = @{
                fieldName1 = $null
                fieldName2 = $null
            }
            schemaName2 = @{
                fieldName3 = $null
            }
        }
 
    The Custom Schema and it's fields **MUST** exist prior to updating these values for a user otherwise it will return an error.
 
    .EXAMPLE
    Update-GSUser -User john.smith@domain.com -PrimaryEmail johnathan.smith@domain.com -GivenName Johnathan -Suspended:$false
 
    Updates user john.smith@domain.com with a new primary email of "johnathan.smith@domain.com", sets their Given Name to "Johnathan" and unsuspends them. Their previous primary email "john.smith@domain.com" will become an alias on their account automatically
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.User')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High",DefaultParameterSetName = "NamePart")]
    Param
    (
        [parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Alias("Id", "UserKey", "Mail")]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $User,
        [parameter(Mandatory = $false)]
        [String]
        $PrimaryEmail,
        [parameter(Mandatory = $false,ParameterSetName = "NamePart")]
        [String]
        $GivenName,
        [parameter(Mandatory = $false,ParameterSetName = "NamePart")]
        [String]
        $FamilyName,
        [parameter(Mandatory = $false,ParameterSetName = "FullName")]
        [String]
        $FullName,
        [parameter(Mandatory = $false)]
        [SecureString]
        $Password,
        [parameter(Mandatory = $false)]
        [Switch]
        $ChangePasswordAtNextLogin,
        [parameter(Mandatory = $false)]
        [String]
        $OrgUnitPath,
        [parameter(Mandatory = $false)]
        [Switch]
        $Suspended,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserAddress[]]
        $Addresses,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserEmail[]]
        $Emails,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId[]]
        $ExternalIds,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserIm[]]
        $Ims,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserLocation[]]
        $Locations,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization[]]
        $Organizations,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserRelation[]]
        $Relations,
        [parameter(Mandatory = $false)]
        [String]
        $RecoveryEmail,
        [parameter(Mandatory = $false)]
        [String]
        $RecoveryPhone,
        [parameter(Mandatory = $false)]
        [Google.Apis.Admin.Directory.directory_v1.Data.UserPhone[]]
        $Phones,
        [parameter(Mandatory = $false)]
        [Switch]
        $IncludeInGlobalAddressList,
        [parameter(Mandatory = $false)]
        [Switch]
        $IpWhitelisted,
        [parameter(Mandatory = $false)]
        [Switch]
        $IsAdmin,
        [parameter(Mandatory = $false)]
        [Switch]
        $Archived,
        [parameter(Mandatory = $false)]
        [ValidateScript( {
                $hash = $_
                foreach ($schemaName in $hash.Keys) {
                    if ($hash[$schemaName].GetType().Name -ne 'Hashtable') {
                        throw "The CustomSchemas parameter only accepts a hashtable where the value of the top-level values must also be a hashtable. The key '$schemaName' has a value of type '$($hash[$schemaName].GetType().Name)'"
                        $valid = $false
                    }
                    else {
                        $valid = $true
                    }
                }
                $valid
            })]
        [Hashtable]
        $CustomSchemas
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
    }
    Process {
        foreach ($U in $User) {
            try {
                Resolve-Email ([ref]$U)
                if ($PSCmdlet.ShouldProcess("Updating user '$U'")) {
                    Write-Verbose "Updating user '$U'"
                    $userObj = Get-GSUser $U -Verbose:$false
                    $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.User'
                    $name = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserName'
                    $nameUpdated = $false
                    $toClear = @{ }
                    foreach ($prop in $PSBoundParameters.Keys | Where-Object { $body.PSObject.Properties.Name -contains $_ -or $name.PSObject.Properties.Name -contains $_ }) {
                        switch ($prop) {
                            PrimaryEmail {
                                if ($PSBoundParameters[$prop] -notlike "*@*.*") {
                                    $PSBoundParameters[$prop] = "$($PSBoundParameters[$prop])@$($Script:PSGSuite.Domain)"
                                }
                                $body.$prop = $PSBoundParameters[$prop]
                            }
                            GivenName {
                                $name.$prop = $PSBoundParameters[$prop]
                                $nameUpdated = $true
                            }
                            FamilyName {
                                $name.$prop = $PSBoundParameters[$prop]
                                $nameUpdated = $true
                            }
                            FullName {
                                $fName = $PSBoundParameters[$prop]
                                if ($fName -match ',') {
                                    $splitName = ($fName -split ',',2).Trim()
                                    $name.FamilyName = $splitName[0]
                                    $name.GivenName = $splitName[1]
                                }
                                else {
                                    $splitName = ($fName -split ' ').Trim()
                                    $name.FamilyName = $splitName[-1]
                                    $name.GivenName = $splitName[0..$($splitName.Count - 2)] -join " "
                                }
                                $nameUpdated = $true
                            }
                            Password {
                                $body.Password = (New-Object PSCredential "user", $Password).GetNetworkCredential().Password
                            }
                            CustomSchemas {
                                $schemaDict = New-Object 'System.Collections.Generic.Dictionary`2[[System.String],[System.Collections.Generic.IDictionary`2[[System.String],[System.Object]]]]'
                                foreach ($schemaName in $CustomSchemas.Keys) {
                                    $fieldDict = New-Object 'System.Collections.Generic.Dictionary`2[[System.String],[System.Object]]'
                                    $schemaFields = $CustomSchemas[$schemaName]
                                    $schemaFields.Keys | ForEach-Object {
                                        $fieldDict.Add($_, $schemaFields[$_])
                                    }
                                    $schemaDict.Add($schemaName, $fieldDict)
                                }
                                $body.CustomSchemas = $schemaDict
                            }
                            Emails {
                                $emailList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserEmail]'
                                foreach ($email in $Emails) {
                                    $emailList.Add($email)
                                }
                                $body.Emails = $emailList
                            }
                            ExternalIds {
                                if ($null -ne $ExternalIds) {
                                    $extIdList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserExternalId]'
                                    foreach ($extId in $ExternalIds) {
                                        $extIdList.Add($extId)
                                    }
                                    $body.ExternalIds = $extIdList
                                }
                                else {
                                    $toClear['externalIds'] = $null
                                }
                            }
                            Ims {
                                if ($null -ne $Ims) {
                                    $imList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserIm]'
                                    foreach ($im in $Ims) {
                                        $imList.Add($im)
                                    }
                                    $body.Ims = $imList
                                }
                                else {
                                    $toClear['ims'] = $null
                                }
                            }
                            Locations {
                                if ($null -ne $Locations) {
                                    $locationList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserLocation]'
                                    foreach ($loc in $Locations) {
                                        $locationList.Add($loc)
                                    }
                                    $body.Locations = $locationList
                                }
                                else {
                                    $toClear['locations'] = $null
                                }
                            }
                            Organizations {
                                if ($null -ne $Organizations) {
                                    $orgList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserOrganization]'
                                    foreach ($organization in $Organizations) {
                                        $orgList.Add($organization)
                                    }
                                    $body.Organizations = $orgList
                                }
                                else {
                                    $toClear['organizations'] = $null
                                }
                            }
                            Relations {
                                if ($null -ne $Relations) {
                                    $relList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserRelation]'
                                    foreach ($relation in $Relations) {
                                        $relList.Add($relation)
                                    }
                                    $body.Relations = $relList
                                }
                                else {
                                    $toClear['relations'] = $null
                                }
                            }
                            Phones {
                                if ($null -ne $Phones) {
                                    $phoneList = New-Object 'System.Collections.Generic.List`1[Google.Apis.Admin.Directory.directory_v1.Data.UserPhone]'
                                    foreach ($phone in $Phones) {
                                        $phoneList.Add($phone)
                                    }
                                    $body.Phones = $phoneList
                                }
                                else {
                                    $toClear['phones'] = $null
                                }
                            }
                            IsAdmin {
                                if ($userObj.IsAdmin -eq $PSBoundParameters[$prop]) {
                                    Write-Verbose "User '$U' already has IsAdmin set to '$($userObj.IsAdmin)'"
                                }
                                else {
                                    if ($PSCmdlet.ShouldProcess("Updating user '$U' to IsAdmin '$($PSBoundParameters[$prop])'")) {
                                        Write-Verbose "Updating user '$U' to IsAdmin '$($PSBoundParameters[$prop])'"
                                        $adminBody = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserMakeAdmin' -Property @{
                                            Status = $PSBoundParameters[$prop]
                                        }
                                        $request = $service.Users.MakeAdmin($adminBody, $userObj.Id)
                                        $request.Execute()
                                    }
                                }
                            }
                            Default {
                                $body.$prop = $PSBoundParameters[$prop]
                            }
                        }
                    }
                    if ($nameUpdated) {
                        $body.Name = $name
                    }
                    $request = $service.Users.Update($body, $userObj.Id)
                    $request.Execute() | Add-Member -MemberType NoteProperty -Name 'User' -Value $U -PassThru
                    if ($toClear.Keys.Count) {
                        $header = @{
                            Authorization = "Bearer $(Get-GSToken -Scopes "https://www.googleapis.com/auth/admin.directory.user" -Verbose:$false)"
                        }
                        $uri = [Uri]"https://www.googleapis.com/admin/directory/v1/users/$U"
                        Write-Verbose "Clearing out all values for User '$U' on the following properties: [ $($toClear.Keys -join ", ") ]"
                        $null = Invoke-RestMethod -Method Put -Uri $uri -Headers $header -Body $($toClear | ConvertTo-Json -Depth 5 -Compress) -ContentType 'application/json' -Verbose:$false -ErrorAction Stop
                    }
                }
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
                else {
                    Write-Error $_
                }
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSUser'
function Update-GSUserPhoto {
    <#
    .SYNOPSIS
    Updates the photo for the specified user
 
    .DESCRIPTION
    Updates the photo for the specified user
 
    .PARAMETER User
    The primary email or UserID of the user who you are trying to update the photo for. You can exclude the '@domain.com' to insert the Domain in the config or use the special 'me' to indicate the AdminEmail in the config.
 
    .PARAMETER Path
    The path of the photo that you would like to update the user with.
 
    .EXAMPLE
    Update-GSUserPhoto -User me -Path .\myphoto.png
 
    Updates the Google user photo of the AdminEmail with the image at the specified path
    #>

    [OutputType('Google.Apis.Admin.Directory.directory_v1.Data.UserPhoto')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
        [Alias("PrimaryEmail","UserKey","Mail")]
        [ValidateNotNullOrEmpty()]
        [String]
        $User,
        [parameter(Mandatory = $true,Position = 1)]
        [ValidateScript({Test-Path $_})]
        [String]
        $Path
    )
    Begin {
        $serviceParams = @{
            Scope       = 'https://www.googleapis.com/auth/admin.directory.user'
            ServiceType = 'Google.Apis.Admin.Directory.directory_v1.DirectoryService'
        }
        $service = New-GoogleService @serviceParams
        $Path = (Resolve-Path $Path).Path
    }
    Process {
        try {
            if ($User -ceq 'me') {
                $User = $Script:PSGSuite.AdminEmail
            }
            elseif ($User -notlike "*@*.*") {
                $User = "$($User)@$($Script:PSGSuite.Domain)"
            }
            $mimeType = Get-MimeType -File $Path
            $photoData = [System.Convert]::ToBase64String(([System.IO.File]::ReadAllBytes($Path))) | Convert-Base64 -From Base64String -To WebSafeBase64String
            $body = New-Object 'Google.Apis.Admin.Directory.directory_v1.Data.UserPhoto' -Property @{
                PhotoData = $photoData
                MimeType = $mimeType
            }
            Write-Verbose "Updating the photo for User '$User' with file '$Path'"
            $request = $service.Users.Photos.Update($body,$User)
            $request.Execute()
        }
        catch {
            if ($ErrorActionPreference -eq 'Stop') {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            else {
                Write-Error $_
            }
        }
    }
}

Export-ModuleMember -Function 'Update-GSUserPhoto'

Import-GoogleSDK

if ($global:PSGSuiteKey -and $MyInvocation.BoundParameters['Debug']) {
    $prevDebugPref = $DebugPreference
    $DebugPreference = "Continue"
    Write-Debug "`$global:PSGSuiteKey is set to a $($global:PSGSuiteKey.Count * 8)-bit key!"
    $DebugPreference = $prevDebugPref
}

$aliasHash = # Alias => => => => => => => => Function
@{
    'Add-GSDriveFilePermissions'        = 'Add-GSDrivePermission'
    'Export-PSGSuiteConfiguration'      = 'Set-PSGSuiteConfig'
    'Get-GSCalendarEventList'           = 'Get-GSCalendarEvent'
    'Get-GSCalendarResourceList'        = 'Get-GSResourceList'
    'Get-GSDataTransferApplicationList' = 'Get-GSDataTransferApplication'
    'Get-GSDriveFileInfo'               = 'Get-GSDriveFile'
    'Get-GSDriveFilePermissionsList'    = 'Get-GSDrivePermission'
    'Get-GSGmailDelegates'              = 'Get-GSGmailDelegate'
    'Get-GSGmailFilterList'             = 'Get-GSGmailFilter'
    'Get-GSGmailLabelList'              = 'Get-GSGmailLabel'
    'Get-GSGmailMessageInfo'            = 'Get-GSGmailMessage'
    'Get-GSGmailSendAsSettings'         = 'Get-GSGmailSendAsAlias'
    'Get-GSGmailSignature'              = 'Get-GSGmailSendAsAlias'
    'Get-GSGroupList'                   = 'Get-GSGroup'
    'Get-GSGroupMemberList'             = 'Get-GSGroupMember'
    'Get-GSMobileDeviceList'            = 'Get-GSMobileDevice'
    'Get-GSOrganizationalUnitList'      = 'Get-GSOrganizationalUnit'
    'Get-GSOrgUnit'                     = 'Get-GSOrganizationalUnit'
    'Get-GSOrgUnitList'                 = 'Get-GSOrganizationalUnit'
    'Get-GSOU'                          = 'Get-GSOrganizationalUnit'
    'Get-GSResourceList'                = 'Get-GSResource'
    'Get-GSTeamDrive'                   = 'Get-GSDrive'
    'Get-GSTeamDrivesList'              = 'Get-GSDrive'
    'Get-GSUserASPList'                 = 'Get-GSUserASP'
    'Get-GSUserLicenseInfo'             = 'Get-GSUserLicense'
    'Get-GSUserLicenseList'             = 'Get-GSUserLicense'
    'Get-GSUserList'                    = 'Get-GSUser'
    'Get-GSUserSchemaInfo'              = 'Get-GSUserSchema'
    'Get-GSUserSchemaList'              = 'Get-GSUserSchema'
    'Get-GSUserTokenList'               = 'Get-GSUserToken'
    'Import-PSGSuiteConfiguration'      = 'Get-PSGSuiteConfig'
    'Move-GSGmailMessageToTrash'        = 'Remove-GSGmailMessage'
    'New-GSCalendarResource'            = 'New-GSResource'
    'New-GSTeamDrive'                   = 'New-GSDrive'
    'Remove-GSGmailMessageFromTrash'    = 'Restore-GSGmailMessage'
    'Remove-GSTeamDrive'                = 'Remove-GSDrive'
    'Set-PSGSuiteDefaultDomain'         = 'Switch-PSGSuiteConfig'
    'Switch-PSGSuiteDomain'             = 'Switch-PSGSuiteConfig'
    'Update-GSCalendarResource'         = 'Update-GSResource'
    'Update-GSGmailSendAsSettings'      = 'Update-GSGmailSendAsAlias'
    'Update-GSSheetValue'               = 'Export-GSSheet'
    'Update-GSTeamDrive'                = 'Update-GSDrive'
}

foreach ($key in $aliasHash.Keys) {
    try {
        New-Alias -Name $key -Value $aliasHash[$key] -Force
    }
    catch {
        Write-Error "[ALIAS: $($key)] $($_.Exception.Message.ToString())"
    }
}

Export-ModuleMember -Alias '*'

if (!(Test-Path (Join-Path "~" ".scrthq"))) {
    New-Item -Path (Join-Path "~" ".scrthq") -ItemType Directory -Force | Out-Null
}

if ($PSVersionTable.ContainsKey('PSEdition') -and $PSVersionTable.PSEdition -eq 'Core' -and !$Global:PSGSuiteKey -and !$IsWindows) {
    if (!(Test-Path (Join-Path (Join-Path "~" ".scrthq") "BlockCoreCLREncryptionWarning.txt"))) {
        Write-Warning "CoreCLR does not support DPAPI encryption! Setting a basic AES key to prevent errors. Please create a unique key as soon as possible as this will only obfuscate secrets from plain text in the Configuration, the key is not secure as is. If you would like to prevent this message from displaying in the future, run the following command:

Block-CoreCLREncryptionWarning
"

    }
    $Global:PSGSuiteKey = [Byte[]]@(1..16)
    $ConfigScope = "User"
}

if ($Global:PSGSuiteKey -is [System.Security.SecureString]) {
    $Method = "SecureString"
    if (!$ConfigScope) {
        $ConfigScope = "Machine"
    }
}
elseif ($Global:PSGSuiteKey -is [System.Byte[]]) {
    $Method = "AES Key"
    if (!$ConfigScope) {
        $ConfigScope = "Machine"
    }
}
else {
    $Method = "DPAPI"
    $ConfigScope = "User"
}

Add-MetadataConverter -Converters @{
    [SecureString] = {
        $encParams = @{}
        if ($Global:PSGSuiteKey -is [System.Byte[]]) {
            $encParams["Key"] = $Global:PSGSuiteKey
        }
        elseif ($Global:PSGSuiteKey -is [System.Security.SecureString]) {
            $encParams["SecureKey"] = $Global:PSGSuiteKey
        }
        'ConvertTo-SecureString "{0}"' -f (ConvertFrom-SecureString $_ @encParams)
    }
    "Secure" = {
        param([string]$String)
        $encParams = @{}
        if ($Global:PSGSuiteKey -is [System.Byte[]]) {
            $encParams["Key"] = $Global:PSGSuiteKey
        }
        elseif ($Global:PSGSuiteKey -is [System.Security.SecureString]) {
            $encParams["SecureKey"] = $Global:PSGSuiteKey
        }
        ConvertTo-SecureString $String @encParams
    }
    "ConvertTo-SecureString" = {
        param([string]$String)
        $encParams = @{}
        if ($Global:PSGSuiteKey -is [System.Byte[]]) {
            $encParams["Key"] = $Global:PSGSuiteKey
        }
        elseif ($Global:PSGSuiteKey -is [System.Security.SecureString]) {
            $encParams["SecureKey"] = $Global:PSGSuiteKey
        }
        ConvertTo-SecureString $String @encParams
    }
}

try {
    $confParams = @{
        Scope = $ConfigScope
    }
    if ($ConfigName) {
        $confParams["ConfigName"] = $ConfigName
        $Script:ConfigName = $ConfigName
    }
    try {
        if ($global:PSGSuite) {
            Write-Warning "Using config $(if ($global:PSGSuite.ConfigName){"name '$($global:PSGSuite.ConfigName)' "})found in variable: `$global:PSGSuite"
            Write-Verbose "$(($global:PSGSuite | Format-List | Out-String).Trim())"
            if ($global:PSGSuite -is [System.Collections.Hashtable]) {
                $global:PSGSuite = New-Object PSObject -Property $global:PSGSuite
            }
            $script:PSGSuite = $global:PSGSuite
        }
        else {
            Get-PSGSuiteConfig @confParams -ErrorAction Stop
        }
    }
    catch {
        if (Test-Path "$ModuleRoot\$env:USERNAME-$env:COMPUTERNAME-$env:PSGSuiteDefaultDomain-PSGSuite.xml") {
            Get-PSGSuiteConfig -Path "$ModuleRoot\$env:USERNAME-$env:COMPUTERNAME-$env:PSGSuiteDefaultDomain-PSGSuite.xml" -ErrorAction Stop
            Write-Warning "No Configuration.psd1 found at scope '$ConfigScope'; falling back to legacy XML. If you would like to convert your legacy XML to the newer Configuration.psd1, run the following command:

Get-PSGSuiteConfig -Path '$ModuleRoot\$env:USERNAME-$env:COMPUTERNAME-$env:PSGSuiteDefaultDomain-PSGSuite.xml' -PassThru | Set-PSGSuiteConfig
"

        }
        else {
            Write-Warning "There was no config returned! Please make sure you are using the correct key or have a configuration already saved."
        }
    }
}
catch {
    Write-Warning "There was no config returned! Please make sure you are using the correct key or have a configuration already saved."
}