Click here to Skip to main content
15,894,180 members
Articles / Web Development / IIS

User Friendly ASP.NET Exception Handling

Rate me:
Please Sign up or sign in to vote.
4.80/5 (70 votes)
20 Dec 20049 min read 670.2K   4.3K   344  
A flexible framework for user-friendly ASP.NET exception handling, and automatically notifying developers of problems before users do.
Imports System.Net
Imports System.Net.Sockets
Imports System.Threading
Imports System.Text
Imports System.Diagnostics

''' <summary>
''' a simple class for trivial SMTP mail support, with no COM dependencies.
''' </summary>
''' <remarks>
'''   - plain text or HTML body
'''   - one optional file attachment
'''   - basic retry mechanism
''' 
'''  Jeff Atwood
'''  http://www.codinghorror.com/
''' </remarks>
Friend Class SimpleMail

    ''' <summary>
    ''' A mail message to be sent. The only required properties are To, and Body.
    ''' </summary>
    Public Class SmtpMailMessage
        ''' <summary>
        ''' Address this email came from. Optional.
        ''' If not provided, an email address will be automatically generated based on the machine name. 
        ''' </summary>
        Public From As String
        ''' <summary>
        ''' Address(es) to send email to. Semicolon delimited. Required.
        ''' </summary>
        Public [To] As String
        ''' <summary>
        ''' Subject text for the email. Optional, but recommended.
        ''' </summary>
        Public Subject As String
        ''' <summary>
        ''' Plain text body. Required.
        ''' </summary>
        Public Body As String
        ''' <summary>
        ''' HTML text body. Optional.
        ''' </summary>
        Public BodyHTML As String
        ''' <summary>
        ''' Fully qualified path of the file you want to attach to the email. Optional.
        ''' </summary>
        Public AttachmentPath As String
        ''' <summary>
        ''' String you wish to attach to the email. Intended for large strings. Optional.
        ''' </summary>
        Public AttachmentText As String
        ''' <summary>
        ''' Name of the attachment as shown in the email. Optional.
        ''' </summary>
        Public AttachmentFilename As String
    End Class

    ''' <summary>
    ''' SMTP client used to submit SMTPMailMessage(s)
    ''' </summary>
    Public Class SmtpClient

        Private Const _intBufferSize As Integer = 1024
        Private Const _intResponseTimeExpected As Integer = 10
        Private Const _intResponseTimeMax As Integer = 750
        Private Const _strAddressSeperator As String = ";"
        Private Const _intMaxRetries As Integer = 3
        Private Const _blnPlainTextOnly As Boolean = False

        Private _strDefaultDomain As String = Config.GetString("SmtpDefaultDomain", "")
        Private _strServer As String = Config.GetString("SmtpServer", "")
        Private _intPort As Integer = Config.GetInteger("SmtpPort", 25)
        Private _strUserName As String = Config.GetString("SmtpAuthUser", "")
        Private _strUserPassword As String = Config.GetString("SmtpAuthPassword", "")
        Private _blnDebugMode As Boolean

        Private _intRetries As Integer = 1

        Private _strLastResponse As String

        Public Sub New()
            _blnDebugMode = System.Diagnostics.Debugger.IsAttached
        End Sub

        ''' <summary>
        ''' Authenticating username, if your mail server requires outgoing authentication.
        ''' Leave blank otherwise.
        ''' </summary>
        Public Property AuthUser() As String
            Get
                Return _strUserName
            End Get
            Set(ByVal Value As String)
                _strUserName = Value
            End Set
        End Property

        ''' <summary>
        ''' Authenticating password, if your mail server requires outgoing authentication.
        ''' Leave blank otherwise.
        ''' </summary>
        Public Property AuthPassword() As String
            Get
                Return _strUserPassword
            End Get
            Set(ByVal Value As String)
                _strUserPassword = Value
            End Set
        End Property

        ''' <summary>
        ''' TCP/IP port to use during SMTP communications.
        ''' </summary>
        ''' <remarks>
        ''' Defaults to 25, the standard SMTP port.
        ''' </remarks>
        Public Property Port() As Integer
            Get
                Return _intPort
            End Get
            Set(ByVal Value As Integer)
                _intPort = Value
            End Set
        End Property

        ''' <summary>
        ''' SMTP server name to connect to when sending mail.
        ''' </summary>
        Public Property Server() As String
            Get
                Return _strServer
            End Get
            Set(ByVal Value As String)
                _strServer = Value
            End Set
        End Property

        ''' <summary>
        ''' Default email domain, eg, 'mycompany.com'
        ''' </summary>
        Public Property DefaultDomain() As String
            Get
                Return _strDefaultDomain
            End Get
            Set(ByVal Value As String)
                _strDefaultDomain = Value
            End Set
        End Property

        ''' <summary>
        ''' Returns true if this class is running in a web context
        ''' </summary>
        Private Function IsWebHosted() As Boolean
            Return Not System.Web.HttpContext.Current Is Nothing
        End Function

        ''' <summary>
        ''' send data over the current network connection
        ''' </summary>
        Private Sub SendData(ByVal tcp As TcpClient, ByVal strData As String)
            Dim ns As NetworkStream = tcp.GetStream()
            Dim b(strData.Length) As Byte
            Dim en As New System.Text.UTF8Encoding

            b = en.GetBytes(strData)
            ns.Write(b, 0, b.Length)
        End Sub

        ''' <summary>
        ''' get data from the current network connection
        ''' </summary>
        Private Function GetData(ByVal tcp As TcpClient) As String
            Dim ns As System.Net.Sockets.NetworkStream = tcp.GetStream()

            If ns.DataAvailable Then
                Dim b() As Byte
                Dim intStreamSize As Integer
                b = New Byte(_intBufferSize) {}
                intStreamSize = ns.Read(b, 0, b.Length)
                Dim en As New System.Text.UTF8Encoding
                Return en.GetString(b)
            Else
                Return ""
            End If
        End Function

        ''' <summary>
        ''' issue a required SMTP command
        ''' </summary>
        Private Sub Command(ByVal tcp As TcpClient, ByVal strCommand As String, _
            Optional ByVal strExpectedResponse As String = "250")

            If Not CommandInternal(tcp, strCommand, strExpectedResponse) Then
                tcp.Close()
                Throw New Exception("SMTP server at " & _strServer.ToString & ":" & _intPort.ToString + _
                    " was provided command '" & strCommand & _
                    "', but did not return the expected response '" & strExpectedResponse & "':" _
                    + Environment.NewLine + _strLastResponse)
            End If

        End Sub

        ''' <summary>
        ''' issue a SMTP command
        ''' </summary>
        Private Function CommandInternal(ByVal tcp As TcpClient, ByVal strCommand As String, _
            Optional ByVal strExpectedResponse As String = "250") As Boolean

            Dim intResponseTime As Integer

            '-- send the command over the socket with a trailing cr/lf
            If strCommand.Length > 0 Then
                SendData(tcp, strCommand & Environment.NewLine)
            End If

            '-- wait until we get a response, or time out
            _strLastResponse = ""
            intResponseTime = 0
            Do While (_strLastResponse = "") And (intResponseTime <= _intResponseTimeMax)
                intResponseTime += _intResponseTimeExpected
                _strLastResponse = GetData(tcp)
                Thread.CurrentThread.Sleep(_intResponseTimeExpected)
            Loop

            '-- this is helpful for debugging SMTP problems
            If _blnDebugMode Then
                Debug.WriteLine("SMTP >> " & strCommand & " (after " & intResponseTime.ToString & "ms)")
                Debug.WriteLine("SMTP << " & _strLastResponse)
            End If

            '-- if we have a response, check the first 10 characters for the expected response code
            If _strLastResponse = "" Then
                If _blnDebugMode Then
                    Debug.WriteLine("** EXPECTED RESPONSE " & strExpectedResponse & " NOT RETURNED **")
                End If
                Return False
            Else
                Return (_strLastResponse.IndexOf(strExpectedResponse, 0, 10) <> -1)
            End If
        End Function

        ''' <summary>
        ''' send mail with integrated retry mechanism
        ''' </summary>
        Public Function SendMail(ByVal mail As SmtpMailMessage) As Boolean
            Dim intRetryInterval As Integer = 333
            Try
                SendMailInternal(mail)
            Catch ex As Exception
                _intRetries += 1
                If _blnDebugMode Then
                    Debug.WriteLine("--> SendMail Exception Caught")
                    Debug.WriteLine(ex.Message)
                End If
                If _intRetries <= _intMaxRetries Then
                    Thread.CurrentThread.Sleep(intRetryInterval)
                    SendMail(mail)
                Else
                    Throw
                End If
            End Try
            If _blnDebugMode Then
                Debug.WriteLine("sent after " & _intRetries.ToString)
            End If
            _intRetries = 1
            Return True
        End Function

        ''' <summary>
        ''' send an email via trivial SMTP implementation
        ''' </summary>
        Private Sub SendMailInternal(ByVal mail As SmtpMailMessage)
            Dim iphost As IPHostEntry
            Dim tcp As New TcpClient

            '-- resolve server text name to an IP address
            Try
                iphost = Dns.GetHostByName(_strServer)
            Catch e As Exception
                Throw New Exception("Unable to resolve server name " & _strServer, e)
            End Try

            '-- attempt to connect to the server by IP address and port number
            Try
                tcp.Connect(iphost.AddressList(0), _intPort)
            Catch e As Exception
                Throw New Exception("Unable to connect to SMTP server at " & _strServer.ToString & ":" & _intPort.ToString, e)
            End Try

            '-- make sure we get the SMTP welcome message
            Command(tcp, "", "220")
            Command(tcp, "HELO " & Environment.MachineName)

            '--
            '-- authenticate if we have username and password
            '-- http://www.ietf.org/rfc/rfc2554.txt
            '--
            If Len(_strUserName & _strUserPassword) > 0 Then
                Command(tcp, "auth login", "334 VXNlcm5hbWU6") 'VXNlcm5hbWU6=base64'Username:'
                Command(tcp, ToBase64(_strUserName), "334 UGFzc3dvcmQ6") 'UGFzc3dvcmQ6=base64'Password:'
                Command(tcp, ToBase64(_strUserPassword), "235")
            End If

            If mail.From = "" Then
                If IsWebHosted() Then
                    mail.From = System.Web.HttpContext.Current.Request.ServerVariables("server_name") & _
                        "@" & _strDefaultDomain
                Else
                    mail.From = System.AppDomain.CurrentDomain.FriendlyName.ToLower & "." & _
                        Environment.MachineName.ToLower & "@" & _strDefaultDomain
                End If
            End If
            Command(tcp, "MAIL FROM: <" & mail.From & ">")

            '-- send email to more than one recipient
            Dim strRecipients() As String = mail.To.Split(_strAddressSeperator.ToCharArray)
            For Each strRecipient As String In strRecipients
                Command(tcp, "RCPT TO: <" & strRecipient & ">")
            Next

            Command(tcp, "DATA", "354")

            Dim sb As New StringBuilder
            With sb
                '-- write common email headers
                .Append("To: " & mail.To + Environment.NewLine)
                .Append("From: " & mail.From + Environment.NewLine)
                .Append("Subject: " & mail.Subject + Environment.NewLine)

                If _blnPlainTextOnly Then
                    '-- write plain text body
                    .Append(Environment.NewLine + mail.Body + Environment.NewLine)
                Else
                    Dim strContentType As String
                    '-- typical case; mixed content will be displayed side-by-side
                    strContentType = "multipart/mixed"
                    '-- unusual case; text and HTML body are both included, let the reader determine which it can handle
                    If mail.Body <> "" And mail.BodyHTML <> "" Then
                        strContentType = "multipart/alternative"
                    End If

                    .Append("MIME-Version: 1.0" & Environment.NewLine)
                    .Append("Content-Type: " & strContentType & "; boundary=""NextMimePart""" & Environment.NewLine)
                    .Append("Content-Transfer-Encoding: 7bit" & Environment.NewLine)
                    ' -- default content (for non-MIME compliant email clients, should be extremely rare)
                    .Append("This message is in MIME format. Since your mail reader does not understand " & Environment.NewLine)
                    .Append("this format, some or all of this message may not be legible." & Environment.NewLine)
                    '-- handle text body (if any)
                    If mail.Body <> "" Then
                        .Append(Environment.NewLine & "--NextMimePart" & Environment.NewLine)
                        .Append("Content-Type: text/plain;" & Environment.NewLine)
                        .Append(Environment.NewLine + mail.Body + Environment.NewLine)
                    End If
                    ' -- handle HTML body (if any)
                    If mail.BodyHTML <> "" Then
                        .Append(Environment.NewLine & "--NextMimePart" & Environment.NewLine)
                        .Append("Content-Type: text/html; charset=iso-8859-1" & Environment.NewLine)
                        .Append(Environment.NewLine + mail.BodyHTML + Environment.NewLine)
                    End If
                    '-- handle attachment (if any)
                    If mail.AttachmentPath <> "" Then
                        .Append(FileToMimeString(mail.AttachmentPath, mail.AttachmentFilename))
                    End If
                    If mail.AttachmentText <> "" Then
                        .Append(ToMimeString(mail.AttachmentText, mail.AttachmentFilename))
                    End If
                End If
                '-- <crlf>.<crlf> marks end of message content
                .Append(Environment.NewLine & "." & Environment.NewLine)
            End With

            Command(tcp, sb.ToString)
            Command(tcp, "QUIT", "")
            tcp.Close()
        End Sub

        ''' <summary>
        ''' returns MIME header section string
        ''' </summary>
        Private Function MimeHeaderString(ByVal strFilename As String) As String
            Dim sb As New StringBuilder
            If strFilename Is Nothing Or strFilename = "" Then strFilename = "attachment.txt"
            With sb
                .Append(Environment.NewLine & "--NextMimePart" & Environment.NewLine)
                .Append("Content-Type: application/octet-stream; name=""" & strFilename & """" & Environment.NewLine)
                .Append("Content-Transfer-Encoding: base64" & Environment.NewLine)
                .Append("Content-Disposition: attachment; filename=""" & strFilename & """" & Environment.NewLine)
                .Append(Environment.NewLine)
            End With
            Return sb.ToString
        End Function

        ''' <summary>
        ''' turn string into a MIME attachment string
        ''' </summary>
        Private Function ToMimeString(ByVal strAny As String, Optional ByVal strFilename As String = "Attachment.txt") As String
            Dim sb As New StringBuilder
            Const intLineWidth As Integer = 75

            strAny = Convert.ToBase64String(System.Text.Encoding.Default.GetBytes(strAny))

            sb.Append(MimeHeaderString(strFilename))
            Dim i As Integer
            Dim c As Integer
            For i = 0 To strAny.Length - 1
                c += 1
                sb.Append(strAny.Substring(i, 1))
                If c = intLineWidth - 1 Then
                    c = 0
                    sb.Append(Environment.NewLine)
                End If
            Next
            Return sb.ToString
        End Function

        ''' <summary>
        ''' turn a file into a MIME attachment string
        ''' </summary>
        Private Function FileToMimeString(ByVal strFilepath As String, Optional ByVal strFileName As String = "") As String

            Dim fs As System.IO.FileStream
            Dim sb As New StringBuilder
            '-- note that chunk size is equal to maximum line width
            Const intChunkSize As Integer = 75
            Dim bytRead(intChunkSize) As Byte
            Dim intRead As Integer

            If strFileName Is Nothing Or strFileName = "" Then
                '-- get just the filename out of the path
                strFileName = System.IO.Path.GetFileName(strFilepath)
            End If

            sb.Append(MimeHeaderString(strFileName))

            fs = New System.IO.FileStream(strFilepath, System.IO.FileMode.Open, System.IO.FileAccess.Read)
            intRead = fs.Read(bytRead, 0, intChunkSize)
            Do While intRead > 0
                sb.Append(System.Convert.ToBase64String(bytRead, 0, intRead))
                sb.Append(Environment.NewLine)
                intRead = fs.Read(bytRead, 0, intChunkSize)
            Loop
            fs.Close()

            Return sb.ToString
        End Function

        ''' <summary>
        ''' Encodes a string as Base64
        ''' </summary>
        Private Function ToBase64(ByVal data As String) As String
            Dim Encoder As System.Text.UTF8Encoding = New System.Text.UTF8Encoding
            Return Convert.ToBase64String(Encoder.GetBytes(data))
        End Function

    End Class

End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
My name is Jeff Atwood. I live in Berkeley, CA with my wife, two cats, and far more computers than I care to mention. My first computer was the Texas Instruments TI-99/4a. I've been a Microsoft Windows developer since 1992; primarily in VB. I am particularly interested in best practices and human factors in software development, as represented in my recommended developer reading list. I also have a coding and human factors related blog at www.codinghorror.com.

Comments and Discussions