In Powershell, I'm trying to create a zip file from a large text string, attach it to an email message, and send it along. The trick is that I need to do it without using local storage or disk resources, creating the constructs in memory.
I have two pieces where I'm stuck.
Because I can't get past the first issue of creating the zip, I haven't been able to attempt to attach the zip to the email.
$strTextToZip = '<html><head><title>Log report</title></head><body><p>Paragraph</p><ul><li>first</li><li>second</li></ul></body></html>'
Add-Type -Assembly 'System.IO.Compression'
Add-Type -Assembly 'System.IO.Compression.FileSystem'
# step 1 - create the memorystream for the zip archive
$memoryStream = New-Object System.IO.Memorystream
$zipArchive = New-Object System.IO.Compression.ZipArchive($memoryStream, [System.IO.Compression.ZipArchiveMode]::Create, $true)
# step 2 - create the zip archive's entry for the log file and open it for writing
$htmlFile = $zipArchive.CreateEntry("log.html", 0)
$entryStream = $htmlFile.Open()
# step 3 - write the HTML file to the zip archive entry
$fileStream = [System.IO.StreamWriter]::new( $entryStream )
$fileStream.Write( $strTextToZip )
$fileStream.close()
# step 4 - create mail message
$msg = New-Object Net.Mail.MailMessage($smtp['from'], $smtp['to'], $smtp['subject'], $smtp['body'])
$msg.IsBodyHTML = $true
# step 5 - add attachment
$attachment = New-Object Net.Mail.Attachment $memoryStream, "application/zip"
$msg.Attachments.Add($attachment)
# step 6 - send email
$smtpClient = New-Object Net.Mail.SmtpClient($smtp['server'])
$smtpClient.Send($msg)
The streaming in step 3 for the entry doesn't populate the variable. Also, once populated successfully, I'm not sure how to close the streams and keep the content available for the email attachment. Lastly, I think the Add
for the Net.Mail.Attachment
in step 5 should successfully copy the zip to the message, but any pointers here would be welcome as well.
For the sake of at least giving a proper guidance on the first item, how to create a Zip in Memory without a file. You're pretty close, this is how it's done, older version of Compress-Archive
uses a similar logic with a MemoryStream
and it's worth noting because of this it has it's 2Gb limitation, see this answer for more details.
using namespace System.Text
using namespace System.IO
using namespace System.IO.Compression
using namespace System.Net.Mail
using namespace System.Net.Mime
Add-Type -Assembly System.IO.Compression, System.IO.Compression.FileSystem
$memStream = [MemoryStream]::new()
$zipArchive = [ZipArchive]::new($memStream, [ZipArchiveMode]::Create, $true)
$entry = $zipArchive.CreateEntry('log.html', [CompressionLevel]::Optimal)
$wrappedStream = $entry.Open()
$writer = [StreamWriter] $wrappedStream
$writer.AutoFlush = $true
$writer.Write(@'
<html>
<head>
<title>Log report</title>
</head>
<body>
<p>Paragraph</p>
<ul>
<li>first</li>
<li>second</li>
</ul>
</body>
</html>
'@)
$writer, $wrappedStream, $zipArchive | ForEach-Object Dispose
Up until this point, the first question is answered, now $memStream
holds your Zip file in memory, if we were to test if this is true (obviously we would need a file, since I don't have an smtp server available for further testing):
$file = (New-Item 'test.zip' -ItemType File -Force).OpenWrite()
$memStream.Flush()
$memStream.WriteTo($file)
# We dispose here for demonstration purposes
# in the actual code you should Dispose as last step (I think, untested)
$file, $memStream | ForEach-Object Dispose
Resulting Zip file would become:
Now trying to answer the 2nd question, this is based on a hunch and I think your logic is sound already, Attachment
class has a .ctor
that supports a stream Attachment(Stream, ContentType)
so I feel this should work properly though I can't personally test it:
$msg = [MailMessage]::new($smtp['from'], $smtp['to'], $smtp['subject'], $smtp['body'])
$msg.IsBodyHTML = $true
# EDIT:
# Based on OP's comments, this is the correct order to follow
# first Flush()
$memStream.Flush()
# then
$memStream.Position = 0
# now we can attach the memstream
# Use `Attachment(Stream, String, String)` ctor instead
# so you can give it a name
$msg.Attachments.Add([Attachment]::new(
$memStream,
'nameyourziphere.zip',
[ContentType]::new('application/zip')
))
$memStream.Close() # I think you can close here then send
$smtpClient = [SmtpClient]::new($smtp['server'])
$smtpClient.EnableSsl = $true
$smtpClient.Send($msg)
$msg, $smtpClient | ForEach-Object Dispose