Search code examples
powershellemailzipmemorystreamsystem.io.compression

In Powershell, using only memory (no disk storage available), how can I create a zip archive of a large text file and attach it to an email message?


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.

  1. The steps I'm taking below don't write any content to the zip variable or its entry.
  2. How do I attach the zip, once it's complete, to the email?

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.


Solution

  • 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:

    demo

    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