PopImap.ps1

class Message
{
  [string]$sender
  [string]$text
  [datetime]$timeStamp = [datetime]::Now

  [string]ToString()
  {
    return "{0} {1} {2}" -f $this.timeStamp.ToString("O"), $this.sender, $this.text
  }
}

class MessageReceiver
{
  [void]Receive([Message]$msg)
  {
    Write-Host $msg
  }
}

class FileMessageReceiver : MessageReceiver
{
  [string]$path

  FileMessageReceiver([string]$path)
  {
    $this.path = $path
    $folder = Split-Path -Path $path
    if ($folder -and !(Test-Path -Path $folder))
    {
      $null = New-Item -ItemType Directory -Path $folder
    }
  }

  [void]Receive([Message]$msg)
  {
    Add-Content -Path $this.path -Value $msg
  }
}

class MemoryMessageReceiver : MessageReceiver
{
  $Store = [System.Collections.Generic.List[Message]]::new()

  MemoryMessageReceiver()
  {
  }

  [void]Receive([Message]$msg)
  {
    $this.Store.Add($msg)
  }
}

class TcpClient
{
  [string]$server
  [int]$port
  [System.IO.StreamReader]$reader
  [System.IO.StreamWriter]$writer
  [System.Net.Sockets.TcpClient]$client
  $MessageReceivers = [System.Collections.Generic.List[MessageReceiver]]::new()

  TcpClient([string]$server, [int]$port)
  {
    $this.server = $server
    $this.port = $port
  }

  [void]Close()
  {
    if ($this.client)
    {
      $this.client.Close()
      $this.SendMessage("C", "Connection is closed.")
    }
  }

  [void]Connect()
  {
    $this.client = New-Object System.Net.Sockets.TcpClient($this.server, $this.port)
    $this.client.Client.SetSocketOption([System.Net.Sockets.SocketOptionLevel]"Socket", [System.Net.Sockets.SocketOptionName]"KeepAlive", $true)
    $strm = $this.client.GetStream()
    [System.Net.Security.RemoteCertificateValidationCallback]$c={return $true}
    $strm = New-Object System.Net.Security.SslStream($strm, $false, $c)
    $strm.AuthenticateAsClient($this.server)
    $this.reader = New-Object System.IO.StreamReader($strm, [System.Text.Encoding]::ASCII)
    $this.writer = New-Object System.IO.StreamWriter($strm, [System.Text.Encoding]::ASCII)
    $this.writer.NewLine = "`r`n"
    $this.writer.AutoFlush = $true
    $line = $this.reader.ReadLine()
    $this.SendMessage("S", $line)
  }

  [void]SendMessage([string]$sender, [string]$text)
  {
    [Message]$msg = @{
      text = $text
      sender = $sender
    }
    foreach($receiver in $this.MessageReceivers)
    {
      $receiver.Receive($msg)
    }
  }
}

function Get-O365Token([string]$accessToken, [string]$upn)
{
  [char]$ctrlA = 1
  $token = "user=" + $upn + $ctrlA + "auth=Bearer " + $accessToken + $ctrlA + $ctrlA
  $bytes = [System.Text.Encoding]::ASCII.GetBytes($token)
  $encodedToken = [Convert]::ToBase64String($bytes)
  return $encodedToken
}

class ImapClient : TcpClient
{
  [int]$tag = 0

  ImapClient([string]$server, [int]$port) : base($server, $port)
  {
  }

  [bool]ExecuteCommand([string]$cmd)
  {
    $tagText = $this.getNextTagText()
    $cmdText = [string]::Format("{0} {1}", $tagText, $cmd)
    $this.ExecutePartial($cmdText)
    do 
    {
      $line = $this.ReadPartial()
    }
    while(!$line.StartsWith($tagText + " "))
    $parts = $line.Split(" ")
    return $parts.Length -gt 1 -and $parts[1] -eq "OK"
  }

  [void]ExecutePartial($cmd)
  {
    $this.SendMessage("C", $this.redact($cmd))
    $this.writer.WriteLine($cmd)
  }

  [string]ReadPartial()
  {
    $result = $this.reader.ReadLine()
    $this.SendMessage("S", $result)
    return $result
  }

  [bool]SaveEmail([string]$folder, [string]$content)
  {
    $t = $this.getNextTagText()
    $cmd = "$t APPEND $folder {" + $content.Length + "}"
    $this.ExecutePartial($cmd)
    $line = $this.ReadPartial()
    if ($line -and $line.StartsWith("+ Ready for additional command text."))
    {
      $this.ExecutePartial($content)
      $line = $this.ReadPartial()
      $parts = $line.Split(" ")
      return $parts.Length -gt 1 -and $parts[1] -eq "OK"
    }
    return $false
  }

  [bool]Logon([string]$user, [string]$pass)
  {
    return $this.ExecuteCommand("login $user $pass")
  }

  [bool]XOauth2Authenticate([string]$oAuthToken)
  {
    $cmd = "AUTHENTICATE XOAUTH2 $oAuthToken"
      return $this.ExecuteCommand($cmd)
  }

  [bool]O365Authenticate([string]$accessToken, [string]$upn)
  {
    $token = Get-O365Token -accessToken $accessToken -upn $upn
    return $this.XOauth2Authenticate($token)
  }

  [string]redact([string]$text)
  {
    $parts = $text.Split(" ")
    if ($parts.Length -ge 4 -and $parts[1] -eq "login")
    {
      $parts[3] = "****"
      return [string]::Join(" ", $parts[0..3])
    }
    return $text
  }

  [string]getNextTagText()
  {
    return (++$this.tag).ToString("D4")
  }
}

class Pop3Client : TcpClient
{
  Pop3Client([string]$server, [int]$port) : base($server, $port)
  {
  }

  [bool]ExecuteCommand([string]$cmd)
  {
    $result = $false
    $this.SendMessage("C", $this.redact($cmd))
    $this.writer.WriteLine($cmd)
    $sb = [System.Text.StringBuilder]::new()
    $line = $this.reader.ReadLine()
    $sb.AppendLine($line)

    $input = $cmd.Split(" ")
    $c1 = $input[0].ToUpper()
    $hasMore = ($c1 -eq "RETR") `
      -or ($c1 -eq "TOP") `
      -or ($c1 -eq "CAPA") `
      -or (($c1 -eq "LIST") -and ($input.Length -eq 1)) `
      -or (($c1 -eq "UIDL") -and ($input.Length -eq 1))
    $result = $line.StartsWith("+")
    if ($hasMore -and $line.StartsWith("+OK"))
    {
      do
      {
        $line = $this.reader.ReadLine()
        $sb.AppendLine($line)
      }
      while($line -ne ".")
    }
    $this.SendMessage("S", $sb.ToString())
    return $result
  }

  [bool]Logon([string]$user, [string]$pass)
  {
    $result = $this.ExecuteCommand("USER $user")
    if ($result)
    {
      $result = $this.ExecuteCommand("PASS $pass")
    }
    return $result
  }

  [bool]XOauth2Authenticate([string]$oAuthToken)
  {
    $result = $this.ExecuteCommand("AUTH XOAUTH2")
    if ($result)
    {
      $result = $this.ExecuteCommand($oAuthToken)
    }
    return $result
  }

  [bool]O365Authenticate([string]$accessToken, [string]$upn)
  {
    $token = Get-O365Token -accessToken $accessToken -upn $upn
    return $this.XOauth2Authenticate($token)
  }

  [string]redact([string]$text)
  {
    if ($text.StartsWith("PASS "))
    {
      return "PASS ****"
    }
    return $text
  }
}

function Add-Output {
  [OutputType([TcpClient])]
  param 
  (
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [TcpClient]$client,
    [Parameter(Mandatory=$true)] [bool]$WithDefaultOutput,
    [Parameter(Mandatory=$true)] [bool]$WithFileOutput,
    [Parameter(Mandatory=$true)] [string]$OutputPath
  )
  process
  {
    if ($WithDefaultOutput)
    {
      $client.MessageReceivers.Add([MessageReceiver]::new())
    }
    if ($WithFileOutput)
    {
      $client.MessageReceivers.Add([FileMessageReceiver]::new($OutputPath))
    }
    return $client
  }
}

<#
.SYNOPSIS
    Get IMAP client to communicate with an IMAP server.
.DESCRIPTION
    This cmdlet returns an ImapClient object.
.EXAMPLE
    PS C:\>$imap = Get-ImapClient -Server "outlook.office365.com" -Port 993
    PS C:\>$imap.Connect()
    PS C:\>$imap.Logon("user@contoso.com", "<password>")
    PS C:\>$imap.ExecuteCommand("list `"INBOX/`" *")
    PS C:\>$imap.Close()
#>

function Get-ImapClient
{
  [OutputType([ImapClient])]
  param
  (
    [Parameter(Mandatory=$true)] [string]$Server,
    [Parameter(Mandatory=$true)] [int]$Port,
    [Parameter(Mandatory=$false)] [bool]$WithDefaultOutput = $true,
    [Parameter(Mandatory=$false)] [bool]$WithFileOutput = $true,
    [Parameter(Mandatory=$false)] [string]$OutputPath = "logs\imap.log"
  )  
  process
  {
    $client = [ImapClient]::new($Server, $Port)
    return $client | Add-Output -WithDefaultOutput $WithDefaultOutput -WithFileOutput $WithFileOutput -OutputPath $OutputPath
  }  
}

<#
.SYNOPSIS
    Get POP3 client to communicate with an POP3 server.
.DESCRIPTION
    This cmdlet returns an Pop3Client object.
.EXAMPLE
    PS C:\>$pop3 = Get-Pop3Client -Server "outlook.office365.com" -Port 995
    PS C:\>$pop3.Connect()
    PS C:\>$pop3.Logon("user@contoso.com", "<password>")
    PS C:\>$pop3.ExecuteCommand("LIST")
    PS C:\>$pop3.Close()
#>

function Get-Pop3Client
{
  [OutputType([Pop3Client])]
  param
  (
    [Parameter(Mandatory=$true)] [string]$Server,
    [Parameter(Mandatory=$true)] [int]$Port,
    [Parameter(Mandatory=$false)] [bool]$WithDefaultOutput = $true,
    [Parameter(Mandatory=$false)] [bool]$WithFileOutput = $true,
    [Parameter(Mandatory=$false)] [string]$OutputPath = "logs\pop3.log"
  )  
  process
  {
    $client = [Pop3Client]::new($Server, $Port)
    return $client | Add-Output -WithDefaultOutput $WithDefaultOutput -WithFileOutput $WithFileOutput -OutputPath $OutputPath
  }  
}