jueves, 10 de mayo de 2012

Vb.Net Zip and Email

Useful for you?

In this post I will show you how I did this zip and email library that helps me to email a file after zip it. There are methods for zipping files and for attaching files to an email and send it through smtp, but, do we really need to save the zipped file before sending it? In this post I will show you how I used the memoryStream to temporarily persist the zipped file and then use this memory to attach the file in an email.

The Projects

First, we will create the required projects and their references in our working area, we will create our vb.Net solution with 2 projects:
  • mailSender: Is a windows forms application that will help us to configure and provide the details for the email that will be sent. This will be the startup project for our solution.
  •  Mailer: Is a class library project that will provide the methods to zip and email our attachments. I've set the default package of this class library to dtorres.Mailer

mailSender Windows Forms

In the mailSender project, we will create a form that might look like this one:

In the configuration tab we will include the email configuration fields, and will default their values to our email configuration:

Finally, a control not seen in this screenshots is a OpenFileDialog that will assist us to browse the file sytem contents in order to create our attachment when clicking the browse button. I've named this form FormMailSender.
We can start programming our browse button to explore and set the attachment path in our text box assigned for that purpose, I've included the following behavior in the browse button:

Private Sub ButtonBrowse_Click(sender As System.Object, e As System.EventArgs) Handles ButtonBrowse.Click

  OpenFileDialogAttachment.FileName = ""
  Dim dr As DialogResult = OpenFileDialogAttachment.ShowDialog()

  If (dr = Windows.Forms.DialogResult.OK) Then
    TextBoxAttachmentPath.Text = OpenFileDialogAttachment.FileName
  End If

End Sub


We will also provide some validation for the data that we are sending to our api for attachment zipping and email:

A validation for email patterns:

Private Function Is_Email(ByVal value As String) As Boolean
  Dim pattern As String = "^[a-zA-Z][\w.-][a-zA-Z0-9]@[a-zA-Z0-9][\w.-][a-zA-Z0-9].[a-zA-Z][a-zA-Z.]*[a-zA-Z]$"
  Dim patternMatch As Match = Regex.Match(value, pattern)

  Is_Email = patternMatch.Success
End Function



A validation for the provided configuration:

Private Function Validate_Configuration() As Boolean

  Validate_Configuration = True

  If Not Is_Email(TextBoxEmail.Text) Then
    Return False
  End If
  If TextBoxUserName.Text = "" Then
    Return False
  End If

  If TextBoxPassword.Text = "" Then
    Return False
  End If

  If TextBoxHost.Text = "" Then
    Return False
  End If

  If TextBoxPort.Text = "" Or Not IsNumeric(TextBoxPort.Text) Then
    Return False
  End If

End Function



A validation for the email content:

Private Function Validate_Email() As Boolean
  Validate_Email = True
  If Not Is_Email(TextBoxTo.Text) Or TextBoxSubject.Text = "" Then
    Validate_Email = False
  End If
  If TextBoxCc.Text <> "" And Not Is_Email(TextBoxCc.Text) Then
    Validate_Email = False
  End If
End Function


Mailer

Mailer is the class library project that we will use to build the email and its zipped attachment. In order to have the ability to zip attachments, we will need the reference to WindowsBase library. Is a .NET library that you can find also in your equivalent path to my library path, depending on your installed version and OS: C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\WindowsBase.dll. First thing: add the WindowsBase library to your Mailer project and add the Mailer project reference to the mailSender windows forms project.
Next, we will create some utility elements that will be used to communicate between the mailSender and the Mailer:
An exception type (MailerException).- This exception type will be thrown each time we want to communicate an exception to the client, in this case, the mailSender project.

Public Class MailerException
  Inherits Exception

  Sub New(ByVal ErrMsg As String)
    MyBase.New(ErrMsg)
  End Sub

  Sub New(ByVal ErrMsg As String, ByVal innerException As Exception)
    MyBase.New(ErrMsg, innerException)
  End Sub

End Class



A configuration type (MailerConfig).- This class will state the contract that should be filled in order to configure the smtp server properties that will help us to send the email. The configuration for now, includes the following properties:
  • From_Email
  • User_name
  • Password
  • Host
  • Port
  • SSL

Public Class MailerConfig

  Dim _from_email As String
  Dim _user_name As String
  Dim _password As String
  Dim _host As String
  Dim _port As Long
  Dim _ssl As Boolean = False

  Public Property From_Email As String
    Get
      Return _from_email
    End Get
    Set(value As String)
      _from_email = value
    End Set
  End Property

  Public Property User_Name As String
    Get
      Return _user_name
    End Get
    Set(value As String)
      _user_name = value
    End Set
  End Property

  Public Property Password As String
    Get
      Return _password
    End Get
    Set(value As String)
      _password = value
    End Set
  End Property

  Public Property Host As String
    Get
      Return _host
    End Get
    Set(value As String)
      _host = value
    End Set
  End Property

  Public Property Port As Long
    Get
      Return _port
    End Get
    Set(value As Long)
      _port = value
    End Set
  End Property

  Public Property SSL As Boolean
    Get
      Return _ssl
    End Get
    Set(value As Boolean)
      _ssl = value
    End Set
  End Property

End Class



An email details type (EmailDetails).- This class will state the details of the email that we will construct and send. The details that we will specify, include the following properties:
  • Email_to
  • Email_cc
  • Email_subject
  • Email_body
  • Attachment_path

Public Class EmailDetails

  Dim _to As String
  Dim _cc As String
  Dim _subject As String
  Dim _body As String
  Dim _attachment_path As String

  Public Property Email_to As String
    Get
      Return _to
    End Get
    Set(value As String)
      _to = value
    End Set
  End Property

  Public Property Email_cc As String
    Get
      Return _cc
    End Get
    Set(value As String)
      _cc = value
    End Set
  End Property

  Public Property Email_subject As String
    Get
      Return _subject
    End Get
    Set(value As String)
      _subject = value
    End Set
  End Property

  Public Property Email_body As String
    Get
      Return _body
    End Get
    Set(value As String)
      _body = value
    End Set
  End Property

  Public Property Attachment_path As String
    Get
      Return _attachment_path
    End Get
    Set(value As String)
      _attachment_path = value
    End Set
  End Property

End Class



Now we will define the methods to zip and email, those methods will be defined in a class that I will call CMailer. The CMailer class will be our library that will publish a method to handle the email sending and will contain the methods required to read the attachment file, zip it and email it. The method that we will define to send the email will be: Public Sub Send_Email(ByVal config As MailerConfig, ByVal details As EmailDetails). You can add this class with this sole method in the library to begin the integration in this way:
First we will add the class to our Mailer project:

Public Class CMailer
  Public Sub Send_Email(ByVal config As MailerConfig, _
      ByVal details As EmailDetails)
    Throw New MailerException("Not implemented yet!")
  End Sub
End Class


Now we will define the action for the send button at the mailSender project, first we will need a few more private helper methods, one to define the configuration object and one to define the email object. In the FormMailSender code add the following private methods:
Get_Mailer_Config(): Defines the configuration object using the provided values in the config tab controls of the form:

Private Function Get_Mailer_Config() As MailerConfig
  Dim config As New MailerConfig()

  config.From_Email = TextBoxEmail.Text
  config.Host = TextBoxHost.Text
  config.Password = TextBoxPassword.Text
  config.Port = Long.Parse(TextBoxPort.Text)
  config.SSL = CheckBoxSSL.Checked
  config.User_Name = TextBoxUserName.Text

  Return config
End Function



Get_Email_Details(): Defines the email content details using the provided values in the email tab controls of the form:

Private Function Get_Email_Details() As EmailDetails
  Dim details As New EmailDetails()

  details.Attachment_path = TextBoxAttachmentPath.Text
  details.Email_body = TextBoxBody.Text
  details.Email_cc = TextBoxCc.Text
  details.Email_subject = TextBoxSubject.Text
  details.Email_to = TextBoxTo.Text

  Return details
End Function



Clean_Details(): Resets the values of the email content:

Private Sub Clean_Details()
  TextBoxAttachmentPath.Text = ""
  TextBoxTo.Text = ""
  TextBoxCc.Text = ""
  TextBoxSubject.Text = ""
  TextBoxBody.Text = ""
End Sub



Now, we will integrate our code in the CMailer class by adding the behavior of the send button click:

Private Sub ButtonSend_Click(sender As System.Object, e As System.EventArgs) Handles ButtonSend.Click

  If Not Validate_Configuration() Then
    MessageBox.Show("Email configuration is not well formed", "Email Configuration Error", MessageBoxButtons.OK, MessageBoxIcon.Warning)
    Return
  End If
  If Not Validate_Email() Then
    MessageBox.Show("Email details are not well formed", "Email Details Error", MessageBoxButtons.OK, MessageBoxIcon.Warning)
    Return
  End If

  Cursor = Cursors.WaitCursor
  Try
    Dim mailer As New CMailer()
    mailer.Send_Email(Get_Mailer_Config(), Get_Email_Details())
    MessageBox.Show("Email successfully sent", "Fine", MessageBoxButtons.OK, MessageBoxIcon.Information)
    Clean_Details()
  Catch ex As Exception
    MessageBox.Show("Unable to send the email due to the following exception:" & _
      vbCr & ex.Message, "Error Sending Email", MessageBoxButtons.OK, MessageBoxIcon.Error)
  Finally
    Cursor = Cursors.Default
  End Try

End Sub



For now, this program will always throw for you the error message that the Send_Email method hasn't be implemented yet. Next we will implement the zip and email methods.

Zip And Email

Lets get back to our CMailer class and provide some private methods that will help us with the zip and email activities. The first method that we will create is the Read_File(ByVal fileName As String) As Byte(). The Read_File(ByVal fileName As String) As Byte() method will read a file from the system storage and keep its bytes in an array:

Private Function Read_File(ByVal fileName As String) As Byte()
  If Not File.Exists(fileName) Then
    Throw New MailerException("File Not Found: [" & fileName & "]")
  End If

  Try
    Using fs As FileStream = New FileStream(fileName, FileMode.Open, FileAccess.Read)
      Read_File = New Byte((fs.Length) - 1) {}
      Dim bytesToRead As Integer = CType(fs.Length, Integer)
      Dim bytesRead As Integer = 0

      While (bytesToRead > 0)
        Dim n As Integer = fs.Read(Read_File, bytesRead, bytesToRead)
        ' Check EOF To break the look
        If n = 0 Then
          Exit While
        End If

        bytesRead = bytesRead + n
        bytesToRead = bytesToRead - n
      End While

    End Using
  Catch ex As Exception
    Throw New MailerException("Exception while reading the file [" & _
      fileName & "]: [" & ex.Message & "]", ex)
  End Try

End Function




The Sub zipFile(ByRef zip As Package, ByVal fBytes As Byte(), ByVal fileName As String) method will zip a provided byte array into a zip file:

Private Sub zipFile(ByRef zip As Package, ByVal fBytes As Byte(), ByVal fileName As String)
  Dim zipUri As String = String.Concat("/", fileName)
  Dim partUri As New Uri(zipUri, UriKind.Relative)
  Dim contentType As String = Net.Mime.MediaTypeNames.Application.Zip

  Dim pkgPart As PackagePart = zip.CreatePart(partUri, contentType, CompressionOption.Normal)
  pkgPart.GetStream().Write(fBytes, 0, fBytes.Length)
End Sub


Please note the usage of the fileName, you will need to provide a uri that specifies a relative path inside the zip where the file will be placed.

Now we will provide the behavior to the Public Sub Send_Email(ByVal config As MailerConfig, ByVal details As EmailDetails) previously defined in our CMailer class:

Public Sub Send_Email(ByVal config As MailerConfig, _
      ByVal details As EmailDetails)
  ' Read File
  Dim b As Byte() = Read_File(details.Attachment_path)
  Dim fileName As String = Path.GetFileName(details.Attachment_path).Replace(" ", "_")

  ' Zip File
  Dim ms As New MemoryStream()
  Dim zip As Package = ZipPackage.Open(ms, IO.FileMode.Create)
  zipFile(zip, b, fileName)
  zip.Close()

  ' Create Email
  Dim msg As MailMessage = Create_Email(details)
  msg.From = New MailAddress(config.From_Email)

  ' Create Attachment with zip memory stream
  Dim ct As New ContentType()
  ct.MediaType = MediaTypeNames.Application.Zip
  ct.Name = Path.GetFileNameWithoutExtension(fileName) & ".zip"

  ms.Seek(0, SeekOrigin.Begin)
  Dim att As New Attachment(ms, ct)

  msg.Attachments.Add(att)

  ' Connect and send through smtp
  Dim SmtpServer As New SmtpClient()
  SmtpServer.Credentials = New Net.NetworkCredential(config.User_Name, config.Password)
  SmtpServer.Host = config.Host
  SmtpServer.Port = config.Port
  SmtpServer.EnableSsl = config.SSL

  SmtpServer.Send(msg)

  ms.Close()
  ms.Dispose()
End Sub



Note how I'm using the MemoryStream object (ms) to create the zip package in the line: Dim zip As Package = ZipPackage.Open(ms, IO.FileMode.Create), this memory stream is fully written when closing the zip object. Then we use the same object to create the attachment in the line: Dim att As New Attachment(ms, ct). Please note that we have to position the cursor at the begin of the stream before using it to provide the attachment.
In the comments I will provide the link to the source code once I upload it to a public repository.
Cheers.

1 comentario:

  1. Check the source code in gitHub: https://github.com/diego-torres/mailSender

    ResponderBorrar