I am trying to build an error-handling function that will do the following:
- Generate an e-mail message (
System.Net.Mail.MailMessage
object) with an attachment - Save that
MailMessage
to a file on disk (.eml
) - Open the saved file in a new mail window with the user's mail client so that they can send it (or not if they so choose)
I'm doing all of this so that I don't have to code in a specific SMTP server and set of credentials (I've had no luck in figuring out how to use other methods for a "generic" system like I want). What I'm trying to do is basically emulate the functionality of a mailto:
link on an HTML page but from a desktop application and with one or more attachments.
I found this extension method on CodeProject (Adding Save() functionality to System.Net.Mail.MailMessage) for saving the Net.Mail.MailMessage
object to a .eml
file (converted to VB.NET):
<Extension()>
Public Sub Save(ByVal OriginalMessage As Net.Mail.MailMessage, ByVal EMLFileName As String, ByVal Optional AddUnsentHeader As Boolean = True)
Try
Using EMLFileStream As IO.FileStream = IO.File.Open(EMLFileName, IO.FileMode.Create)
If AddUnsentHeader Then
' Add the "unsent header" so that the message can be opened in "edit" mode.
Using EMLWriter As New IO.BinaryWriter(EMLFileStream)
EMLWriter.Write(Text.Encoding.UTF8.GetBytes("X-Unsent: 1" & Environment.NewLine))
End Using
End If
Dim MailAssembly As Assembly = New Net.Mail.SmtpClient().GetType().Assembly
Dim MailWriterType As Type = MailAssembly.[GetType]("System.Net.Mail.MailWriter")
' Get reflection info for MailWriter constructor
Dim MailWriterContructor As ConstructorInfo = MailWriterType.GetConstructor(BindingFlags.Instance Or BindingFlags.NonPublic, Nothing, New Type() {GetType(IO.Stream)}, Nothing)
' Construct the MailWriter object with our FileStream
Dim MailWriter As Object = MailWriterContructor.Invoke(New Object() {EMLFileStream})
' Get reflection info for Send() method on MailMessage
Dim SendMethod As MethodInfo = New Net.Mail.MailMessage().GetType().GetMethod("Send", BindingFlags.Instance Or BindingFlags.NonPublic)
' Call method passing in MailWriter
SendMethod.Invoke(OriginalMessage, BindingFlags.Instance Or BindingFlags.NonPublic, Nothing, New Object() {MailWriter, True, True}, Nothing)
' Finally get reflection info for Close() method on our MailWriter
Dim CloseMethod As MethodInfo = MailWriter.GetType().GetMethod("Close", BindingFlags.Instance Or BindingFlags.NonPublic)
' Call close method
CloseMethod.Invoke(MailWriter, BindingFlags.Instance Or BindingFlags.NonPublic, Nothing, New Object() {}, Nothing)
End Using
Catch ex As Exception
' DO SOMETHING
End Try
End Sub
However, when I call this extension method with a MailMessage
object that has an attachment (a compressed ZIP file), it throws an exception when it tries to execute the SendMethod.Invoke()
line. The .Message
property from the inner exception states Cannot access a closed file.
The .eml
file is created on disk, but it's completely empty.
I also tried using a variation posted in the comments of that thread that uses a MemoryStream
instead of the FileStream
object at the top of the method and then writing that out with the File.WriteAllText()
method, but it throws a similar exception at the same point (before it can hit the WriteAllText()
method) with an inner exception .Message
value of Cannot access a closed Stream.
I'm calling the extension method after building my MailMessage
like this (redacted for the sake of brevity):
Private Sub SendExceptionLog(ByVal LogFile As IO.FileInfo)
Dim TempMailMessageFile As String = $"D:\Temp\Error_{Now.ToString("yyyyMMddHHmmss")}.eml"
Dim DesktopScreenshot As IO.FileInfo = Nothing
If BaseApplicationSettings.LogLevel >= LoggingLevel.TRACE Then
DesktopScreenshot = TakeDesktopScreenshot()
End If
Using Email As New MailMessage
Dim AttachmentList As List(Of IO.FileInfo) = Nothing
Dim Sender As New MailAddress("sender@domain.com", "My Application")
Dim Message As String = String.Empty
With Email
.Subject = "Error in MyApp"
.From = Sender
.IsBodyHtml = True
.To.Add(New MailAddress("developers@domain.com", "Developers"))
Dim MyApp As Reflection.Assembly = Reflection.Assembly.LoadFrom("Common Library.dll")
Using ReportStream As IO.Stream = MyApp.GetManifestResourceStream("MyApp.ErrorReport.html")
If ReportStream IsNot Nothing Then
Using BodyReader As New System.IO.StreamReader(ReportStream)
Message = BodyReader.ReadToEnd
End Using
End If
End Using
Message = Message.Replace("$DateTime$", Format(Now, "d MMMM yyyy - h:mm tt"))
.Body = Message
If LogFile IsNot Nothing Then
AttachmentList = New List(Of IO.FileInfo)
AttachmentList.Add(LogFile)
If DesktopScreenshot IsNot Nothing Then
AttachmentList.Add(DesktopScreenshot)
End If
ElseIf DesktopScreenshot IsNot Nothing Then
AttachmentList = New List(Of IO.FileInfo)
AttachmentList.Add(DesktopScreenshot)
End If
If AttachmentList IsNot Nothing AndAlso AttachmentList.Count > 0 Then
Dim AttachmentZIP As IO.FileInfo = ZIPArchive.CreateZIPArchive(AttachmentList, $"ErrorLog_{Now.ToString("yyyyMMddHHmmss")}.zip", New IO.DirectoryInfo($"{BasePaths.BaseTempPath}\Ichthus\"))
.Attachments.Clear()
.Attachments.Add(New Attachment(AttachmentZIP.FullName))
End If
End With
Email.Save(TempMailMessageFile)
Process.Start(TempMailMessageFile)
End Using
If DesktopScreenshot IsNot Nothing Then
If IO.File.Exists(DesktopScreenshot.FullName) Then
IO.File.Delete(DesktopScreenshot.FullName)
End If
End If
End Sub
I also tried calling the Save()
extension method from a MailMessage
object without any attachments, but it resulted in the exact same exception as above.
Trying to find a way to achieve my goals, I also tried the solution offered in this answer from another StackOverflow question: How to save MailMessage object to disk as *.eml or *.msg file (I just threw the Gmail SMTP relay server in there as a placeholder, but I'm not really sure it matters all that much).
Dim client As New System.Net.Mail.SmtpClient("smtp-relay.gmail.com") With {
.DeliveryMethod = Net.Mail.SmtpDeliveryMethod.SpecifiedPickupDirectory,
.PickupDirectoryLocation = EMLFileName}
client.Send(OriginalMessage)
This method successfully saves the message including the attachment to disk without error, but I have two "issues" with that method:
- It doesn't allow me to specify the file name, so I'd have to go "searching" for the file in order to have my code open it with the
Process.Start
call. - It's not saved as "unsent", so the user wouldn't be able to simply click the Send button from their mail client.
I could probably find a way to work around each of these issues, but I'm guessing it would probably involve a significant part of the code from the first method I listed, so it seems rather pointless if I'm just going to end up doing both.
I'm not sure what File
(or Stream
) is causing the exception during the method invocation, but I assume (based on my testing with the MemoryStream
) that it's the file created by the FileStream
object. As I said, the .eml
file is created on disk, but completely empty - no subject, body, or attachments.
MY ENVIRONMENT:
Microsoft Visual Studio Community 2019
Version 16.11.11
PROJECT INFO
Class Library
.NET Framework 4.7.2
x64 Target CPU
Please let me know if I forgot to include any details that might help to diagnose why this code is throwing this exception.