Checking My E-Mail

 

Duncan Mackenzie
Microsoft Developer Network

March 26, 2003

Summary: Duncan Mackenzie describes how to build a tool that uses the System.Net namespace of the Microsoft .NET Framework to check a POP3 e-mail account for unread messages. (15 printed pages)

Applies to:
   Microsoft® Visual Basic® .NET

Download the source code for this article.

Spouse-Driven Software Design

Coding without any specific goal is like going to the grocery store without a shopping list. Sure, you have a good time, you end up with items—items that might potentially be useful to someone at some point in the future—but you are unlikely to end up with tonight's dinner. Well, that's exactly what I started doing (the coding, not the shopping...) to write this column. I just started coding away, and I wrote some cool stuff (you will just have to take my word for it), but as my deadline got closer and closer, I did not really have anything that was ready for publication. Lucky for me, I received some inspiration after I set up my wife, Laura, with a "new" computer, (okay, it is my old machine), running Microsoft® Windows® XP Home.

Up until now, I had never run a machine with XP Home on it (I generally run Windows XP Pro, connected to a Domain) so I was quite impressed when I saw the Welcome Screen for XP Home (see Figure 1). So was Laura, and she even noticed a feature I hadn't seen; if you are logged in but you've left the system long enough for it to switch back to the logon screen (or you hit Windows Key + L) the screen shows your unread e-mail message count! This was a wonderful feature for her, as she could quickly walk by the computer and see if she had new e-mail messages, without having to enter her password. Ah, the bliss of having provided one's spouse with a geeky computer or electronic solution that they find useful!

Figure 1. Welcome to Windows XP!

Sadly, it always showed the same thing, 7 unread e-mail messages, even though Laura did not have any unread e-mail messages at all. Talk about a setup for disappointment: To be told that you have seven new and potentially exciting messages, only to log on and find that you have none. That blissful feeling was quickly slipping away; I had to do some research, fast.

It turns out those seven unread messages were in her hotmail inbox, but that the value shown on the logon screen was being set using the extremely well-named SHSetUnreadMailCount API call. Now, Laura does not use hotmail for anything beyond MSN® Messenger, so seeing how many unread hotmail messages she has is not very useful. The Microsoft-implanted chip in the back of my neck is beeping like crazy right now at the suggestion, but I decided that this was probably true for many other folks as well. Suddenly I had focus; I could make a small application that would make the logon screen display a more useful value.

Now I should clarify a few details: Microsoft® Outlook® Express also sets this value, so if you are using it for your e-mail you are set, but Outlook does not. What I decided to build would handle connecting to a set of POP3 servers, pulling down the number of available messages, and populating the appropriate registry values. Of course, once I was done with that, I realized that I would likely mess up everything if I ignored Outlook, since that was the mail client Laura was using, so I added Outlook as an additional feature.

So let's break this down, as I like to do, into a set of distinct tasks that I needed to accomplish:

  1. Connect to a POP3 server and check the current # of messages.
  2. Allow you to configure a set of POP3 servers with host and user ID/password information.
  3. Save your server configuration information (including your user ID/password) in a secure fashion.
  4. Write an application that runs in the background and checks each configured server every n minutes.
  5. Update the Windows XP logon screen setting whenever the number of messages changes.
  6. In addition, just for kicks, do all the same stuff with Outlook 2000 or XP.

The full code is available, but I will look at each of these items in order and explain how the code works in each case.

Connecting to the POP3 Servers

I do not write Socket code for a living, and (as someone who uses Microsoft software) you should be happy about that. If my goal were just to make this work, I would go out and find someone else's POP3 component, even if I had to buy it. It would be more efficient and just plain easier. My goal is to show you some Microsoft® .NET code though, so I thought it would be more enjoyable and/or educational for you to watch me wander around in the System.NET namespace and the POP3 spec trying to write my own component.

The first thing I did was to create a little application that did all of the POP3 work in a single Button1_Click routine, and worked in a very procedural fashion (all the code in one big procedure) until I made it work more than once. Before I could use this code in my application though, it needed some tidying. I reorganized it into a proper class and abstracted out some of the specifics around POP3 commands and server information to make it easier for you to change if need be. If I was feeling pretentious, I suppose I would call this the "refactoring" stage, but Microsoft® Word has put a red-squiggle under that term, and I am inclined to agree with its assessment.

If you haven't use the socket classes in .NET, don't be scared—they are quite straightforward. I was able to get a connection working and sending data in only a few minutes. I used an instance of System.Net.Sockets.TcpClient to create a connection to a POP3 server and then grabbed the NetworkStream object once the connection was established.

Public Sub New(ByVal server As String, _
        ByVal port As Integer, _
        ByVal delay As Integer)
    Try
        m_Client = New TcpClient()
        m_Client.Connect(server, port)
        m_NS = m_Client.GetStream()
        m_Delay = delay
        Dim sResponse As String = GetResponse().Trim
        If sResponse.Substring(0, 3) <> "+OK" Then
            Throw New Exception("Connection Failed")
        End If
    Catch se As SocketException
        MsgBox(se.Message & vbCrLf & vbCrLf & se.ToString, _
            MsgBoxStyle.Exclamation, "Socket Exception!")
        Throw New Exception("Connection Failed", se)
    Catch ex As Exception
        MsgBox(ex.Message & vbCrLf & vbCrLf & ex.ToString, _
            MsgBoxStyle.Exclamation, "Exception!")
        Throw New Exception("Connection Failed", ex)
    End Try
End Sub

Once I had the open connection and the Stream, everything after that point was just a matter of sending and receiving text. I abstracted the NetworkStream code to read and write byte arrays into a few higher-level functions: SendCommand and GetResponse. The POP3 spec describes two types of server responses, single-line and multi-line, so I included a MultiLine flag in the parameter list of my two functions.

Private Overloads Function GetResponse() As String
    Return GetResponse(False)
End Function

Private Overloads Function GetResponse( _
        ByVal multiLine As Boolean) As String
    'GetResponse wraps the work of 
    'waiting for a server response to complete
    'Single-Line and Multi-Line responses end
    'differently, so they need slightly different
    'end conditions.
    Dim sOutput As String = ""
    Dim input As Integer
    Dim str(4096) As Byte
    Dim startTime As Date = Now
    Dim endCondition As String

    If multiLine Then
        endCondition = vbCrLf & vbCrLf & "."
    Else
        endCondition = vbCrLf
    End If

    Do
        While m_NS.DataAvailable()
            startTime = Now
            input = m_NS.Read(str, 0, 4096)
            sOutput &= ASCIIEncoding.ASCII.GetChars( _
                str, 0, input)
        End While
    Loop Until sOutput.IndexOf(endCondition) >= 0 _
        Or Now.Subtract(startTime).TotalMilliseconds > Me.m_Delay

    If sOutput.IndexOf(endCondition) < 0 Then
        Return sOutput
    Else
        Return sOutput
    End If
End Function

'SendCommand abstracts sending a string
'and receiving a response
Public Overloads Function SendCommand( _
        ByVal command As String) As String
    Return SendCommand(command, False)
End Function

Public Overloads Function SendCommand( _
        ByVal command As String, _
        ByVal multiLine As Boolean) As String
    Dim user As Byte()
    user = ASCIIEncoding.ASCII.GetBytes(command)
    m_NS.Write(user, 0, user.GetLength(0))
    Return GetResponse(multiLine)
End Function

Note   I could have opened a pair of friendlier stream objects, such as StreamReader and StreamWriter on top of the NetworkStream, but I stuck with the byte array. If you want to see some examples of using StreamReader/StreamWriter with a NetworkStream, check out this MSDN Magazine article by Andrew Duthie.

Once I was sending and receiving successfully, I wanted to work with my sockets code from a higher level (as if someone else wrote it and I did not care how it worked), so I decided to abstract that code into a utility class. Then, in my main POP3Server class, I implemented the simple set of POP3 commands that would enable me to retrieve the list of messages. I logged on to the server, using the USER and PASS commands, got the message count (using STAT), and then listed the headers of each message (using TOP). From the headers I parsed out three important pieces of information: the sender, the subject, and the message ID. Grabbing the message ID provided me with a unique identifier for each message, which allowed me to know when a message was "new" to my program. If I found a new message, I raised an event and passed along the subject and sender information. The code snippet below shows the message header retrieval and raising the new e-mail event, but it makes a lot more sense if you look at in the context of the full source code.

Dim msg As POP3.Message
If msgCount > 0 Then
    Dim i As Integer
    For i = 1 To msgCount
        'get the headers for the message
        'using the TOP command
        response = p3.SendCommand( _
            String.Format(Me.TopCmd, i), True)
        msg = ParseResponse(response)

        Dim msgIndex As Integer = 0
        Dim found As Boolean = False
        'check if this is a new message
        Do While msgIndex < Me.m_Messages.Count And Not found
            If Me.m_Messages(msgIndex).ID = msg.ID Then
                found = True
            End If
            msgIndex += 1
        Loop
        If Not found Then
            Me.m_Messages.Add(msg)
            'if it is new, raise the NewEmail
            'event, passing the sender and subject
            RaiseEvent NewEmail(CObj(Me), _
                New NewEmailEventArgs( _
                    msg.From, msg.Subject, msg.ID))
        End If
    Next
End If

That event was caught by my main application, so this is a good time to move on to the central application code.

Writing an Application for the System Tray

I decided to implement this application as a System Tray icon, because it allowed me an easy and relatively unobtrusive way to provide access to my application's options and notification of new mail.

Figure 2. I created a System Tray icon because you can never have enough of these.

I coded almost all of this application as a single Microsoft® Visual Basic® module (which, for you C# types, is equivalent to a class where all members are static), only using a Form for my options dialog. Coming from Visual Basic 6.0, this is exciting; I wrote an application that uses a system tray icon, a timer, and a context menu without requiring an invisible Form. I really hated having to create invisible forms, especially if they are visible for a brief moment at startup. So I was quite happy to find that I did not need one. In this main module, I created an instance of Hans Blomme's wonderful NotifyIconXP, a context menu and some menu items, and a timer. Then I wired up some event handlers for the menu items, the timer, and even one for the BalloonClick event of the notification icon (even though I don't use it, I thought you might want to).


'servers list 
Dim servers As New POP3ServerCollection()

'timer for polling e-mail servers
Dim checkTimer As New Timer()

'A notifyIcon provides the main UI for the app
'this makes is available for popping up balloons, etc.
Dim ni As HansBlomme.Windows.Forms.NotifyIcon

'general application preferences
Dim appSettings As Settings

'I keep the form available as a module level
'variable so that the ViewOptions menu can avoid
'multiple instances open at once
Dim myFrm As frmOptions

Sub Main()
    'load the server list and settings
    'information from serialized xml files
    Reload()

    'loop through all the loaded
    'servers and attach a handler for
    'the NewE-mail event.
    Dim srv As POP3Server
    For Each srv In servers
        AddHandler srv.NewE-mail, AddressOf NewE-mail
    Next

    'appSettings stores the interval in seconds
    'Timers work in milliseconds, so a little conversion
    'is necessary to make them work together.
    checkTimer.Interval = appSettings.CheckInterval * 1000
    checkTimer.Enabled = True

    'add a handler for the Tick event of the timer
    AddHandler checkTimer.Tick, AddressOf CheckE-mail_Tick

    'Create new Icon, remove HansBlomme section to work with
    'the standard NotifyIcon. Other code changes would also
    'be required
    ni = New HansBlomme.Windows.Forms.NotifyIcon()
    ni.Icon = New Icon(GetType(Main), "mail.ico")
    ni.Visible = True

    'add an event handler for when the user clicks the balloon
    'popped up by the NotifyIcon. The standard NotifyIcon does
    'not provide this event so you will need to remove this 
    'line if you are not using the HansBlomme icon.
    AddHandler ni.BalloonClick, AddressOf NotificationBalloonClicked

    'set up the context menu, including event handlers
    Dim ctxMenu As New ContextMenu()
    ctxMenu.MenuItems.Add("View Options", AddressOf ViewOptionsForm)
    ctxMenu.MenuItems.Add("Exit", AddressOf EndApplication)
    'and assign it to the NotifyIcon so that it will pop up when
    'the icon is right-clicked on.
    ni.ContextMenu = ctxMenu

    'run the application, using Application.Run
    'to create a new message loop
    Application.Run()

    'when the app is exiting
    'hide the notify icon and
    ni.Visible = False

    'save the server info and settings to xml files
    Persist()
End Sub

Saving Settings

At the start of the Main() routine, I called Reload(), and then I called Persist() at the end. These two procedures are how I implemented saving my settings and POP3 server information to disk (in Persist) and loading them up again at startup (in Reload). Serialization is your friend whenever you want to save an object to disk, even a complex structure of objects. The POP3ServerCollection class referenced by this code was generated using the Collection Generator from GotDotNet, and is included in the downloadable source.


Public Sub Persist()
    'Save the Settings object and the collection
    'of POP3 servers to XML files using serialization
    'Note that, unlike the Background Copying article
    'these files are not being saved to Isolated Storage.
    'They could be, I just thought I would be different
    'this time.

    'Figure out the paths for the xml files, notice
    'the use of IO.Path.Combine... much better than just
    'concatenating them together.
    Dim serverSettingsPath As String _
                = IO.Path.Combine( _
                    Application.LocalUserAppDataPath, _
                    "servers.xml")
    Dim settingsPath As String _
        = IO.Path.Combine( _
            Application.LocalUserAppDataPath, _
            "settings.xml")

    Dim myXMLSerializer As Xml.Serialization.XmlSerializer
    Dim settingsFile As IO.StreamWriter

    'save the servers list
    myXMLSerializer _
        = New Xml.Serialization.XmlSerializer( _
            GetType(POP3ServerCollection))
    settingsFile _
        = New IO.StreamWriter(serverSettingsPath)
    myXMLSerializer.Serialize(settingsFile, servers)
    settingsFile.Close()

    'save the settings class
    myXMLSerializer _
        = New Xml.Serialization.XmlSerializer( _
            GetType(Settings))
    settingsFile = New IO.StreamWriter(settingsPath)
    myXMLSerializer.Serialize( _
        settingsFile, appSettings)
    settingsFile.Close()
End Sub

Public Sub Reload()
    'just the reverse of Persist(), this routine
    'loads the two classes from their XML files, or
    'creates new instances if the XML files do not exist
    Dim serverSettingsPath As String _
        = IO.Path.Combine( _
            Application.LocalUserAppDataPath, "servers.xml")
    Dim settingsPath As String _
        = IO.Path.Combine( _
            Application.LocalUserAppDataPath, "settings.xml")

    If IO.File.Exists(serverSettingsPath) Then
        'load servers
        Dim settingsFile As IO.StreamReader _
            = New IO.StreamReader(serverSettingsPath)
        Dim myXMLSerializer As _
            New Xml.Serialization.XmlSerializer( _
                GetType(POP3ServerCollection))
        servers = DirectCast( _
            myXMLSerializer.Deserialize(settingsFile), _
            POP3ServerCollection)
        settingsFile.Close()
    Else
        servers = New POP3ServerCollection()
    End If

    If IO.File.Exists(settingsPath) Then
        'load settings
        Dim settingsFile As IO.StreamReader _
            = New IO.StreamReader(settingsPath)
        Dim myXMLSerializer As _
            New Xml.Serialization.XmlSerializer( _
                GetType(Settings))
        appSettings = DirectCast( _
            myXMLSerializer.Deserialize(settingsFile), _
            Settings)
        settingsFile.Close()
    Else
        appSettings = New Settings()
    End If
End Sub

Storing the User ID/Password

As you can see in the Persist/Reload code, I saved your settings into the local application data path. This puts your information into \Documents and Settings\<userid>\Local Settings\Application Data\POP3\POP3\<version>\ as an XML file. That area should be restricted so that only you can access it, but that would not block someone with administrator access, so I decided I had better do a little bit more with the most sensitive parts of your information. To increase the security, I used DPAPI (courtesy of this little sample) to encrypt the User ID and Password before saving them to disk.


Function EncryptText(ByVal source As String) As String
    'I use the DPAPI component from GotDotNet
    'see the associated article for the link.
    Dim dp As _
        New Dpapi.DataProtector(Dpapi.Store.UserStore)
    Dim sourceBytes As Byte() = _
        System.Text.Encoding.Unicode.GetBytes(source)
    Dim encryptedBytes As Byte()
    encryptedBytes = dp.Encrypt(sourceBytes)
    'I could store binary values, but since I am using
    'XML as a storage mechanism, I prefer to just work
    'with a string. ToBase64String handles that for me
    Return Convert.ToBase64String(encryptedBytes)
End Function

Function DecryptText(ByVal source As String) As String
    'I use the DPAPI component from GotDotNet
    'see the associated article for the link.
    Dim dp As _
        New Dpapi.DataProtector(Dpapi.Store.UserStore)
    Dim sourceBytes As Byte() = _
        Convert.FromBase64String(source)
    Dim decryptedBytes As Byte()
    decryptedBytes = dp.Decrypt(sourceBytes)
    Return System.Text.Encoding.Unicode.GetString(decryptedBytes)
End Function

Of course, encrypting these strings means that I need to do a little bit of extra work to view and/or edit these values through my Options form.

Changing Settings Through an Options Dialog

I have to admit that when I am writing code for myself, I am usually fine with manually changing values in the code, in a .config file, or in the database. That isn't a good habit to get into, though, because at some point you will want to give this code to someone else, and then you are going to have to explain how to change the settings or just bite the bullet and create an options dialog. In this case, I decided to be a bit more proactive, so I created a little dialog and provided a "View Options" menu option off my notification icon.


Sub ViewOptionsForm( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    'Pop up the Options dialog
    Try
        'Check if it is already up,
        'if so, just set the focus to it
        If Not myFrm Is Nothing AndAlso myFrm.Visible Then
            myFrm.Focus()
        Else
            'not already up, create a new copy
            myFrm = New frmOptions()

            'populate the form with data
            myFrm.servers = servers
            myFrm.appSettings = appSettings

            If myFrm.ShowDialog() = DialogResult.OK Then
                'I "clone" the appSettings on the form
                'so you need to pull a copy back when the form
                'closes
                appSettings = myFrm.appSettings
                'hey, you just changed all those settings
                'I'd better save them!
                'This way, even if the app ends in a bad way
                'your changes are still saved.
                Persist()
            End If
            myFrm.Dispose()
            myFrm = Nothing
        End If
    Catch ex As System.Exception
        Debug.WriteLine(ex.ToString)
        Debug.WriteLine(ex.StackTrace)
    End Try
End Sub

To allow the servers collection to be edited, I data bound it to a set of controls and provided a New and a Delete button. The little navigational control is something I wrote for my own use that you might find helpful. I could have just provided a couple of buttons that manipulated the CurrencyManager's Position property if I did not want to use this control.

Figure 3. The Option dialog allows you to set up your POP3 server information.

As I mentioned earlier, in the "Storing the User ID/Password" section, since I have encrypted the User ID and Password, I need to do some work to make those values work in a data bound scenario. I added event handlers for the Parse and Format events of the UserID and Password binding objects, so that these values are automatically decrypted for display and encrypted before being stored into the server collection.


'Format and parse routines to decrypt/encrypt values
Private Sub encryptedBinding_Format( _
       ByVal sender As Object, _
       ByVal e As System.Windows.Forms.ConvertEventArgs)
    If e.Value <> String.Empty Then
        e.Value = Main.DecryptText(CStr(e.Value))
    End If
End Sub

Private Sub encryptedBinding_Parse( _
       ByVal sender As Object, _
       ByVal e As System.Windows.Forms.ConvertEventArgs)
    If e.Value <> String.Empty Then
        e.Value = Main.EncryptText(CStr(e.Value))
    End If
End Sub

The general settings are not data bound; I just pulled them from the Settings class and then pushed the new values back in as needed.


'Instead of databinding the general settings
'I just manually push/pull the values to and from
'the controls.
Private Sub PopulateControls()
    If m_appSettings.CheckInterval _
        > Me.checkInterval.Maximum Then
        m_appSettings.CheckInterval _
            = Me.checkInterval.Maximum
    ElseIf m_appSettings.CheckInterval _
        < Me.checkInterval.Minimum Then
        m_appSettings.CheckInterval _
            = Me.checkInterval.Minimum
    End If
    Me.checkInterval.Value = _
        Me.m_appSettings.CheckInterval
    Me.checkOutlook.Checked = _
        Me.m_appSettings.CheckOutlook
    Me.displayPopups.Checked = _
        Me.m_appSettings.DisplayNewMailPopup
End Sub

Private Sub PullControlValues()
    Me.m_appSettings.CheckInterval = _
        Me.checkInterval.Value
    Me.m_appSettings.CheckOutlook = _
        Me.checkOutlook.Checked
    Me.m_appSettings.DisplayNewMailPopup = _
        Me.displayPopups.Checked
End Sub

Outlook

I almost forgot this one. I had said I would check Outlook's unread messages as well as POP3 accounts. I do not want to reference Outlook's libraries for two reasons:

  • I do not want you to need Outlook to compile this code or to run it.
  • I did not want to create a dependency on a specific version of Outlook.

Without a reference to Outlook, I had to use late binding, which means no Microsoft® IntelliSense® and everything is treated as an object. On the positive side, though, the resulting code should work on Microsoft® Office 2000, Office XP, and even Office 11.


Public Shared Function GetUnreadMessages() As Integer
    Dim OutlookApp As Object
    Try
        OutlookApp = _
            GetObject(, "Outlook.Application")
    Catch
        OutlookApp = Nothing
    End Try
    'Uncomment this to automatically 
    'open Outlook if needed
    'If OutlookApp Is Nothing Then
    '    OutlookApp = _
    '        CreateObject("Outlook.Application")
    'End If

    If Not OutlookApp Is Nothing Then
        '6 is a constant (for the Inbox)
        'exposed by the Outlook library
        'no reference means no library
        'and no constant
        Dim inbox As Object = _
            OutlookApp.Session.GetDefaultFolder(6)
        Return inbox.UnReadItemCount()
    Else
        Return -1
    End If
End Function

Note that this will not work if Outlook is not open. It could if I used CreateObject, but I did not want this little system tray application to force Outlook to be opened every n seconds. If it is not open, then it cannot read the number of unread items in the Outlook inbox. It is important to leave the unread value alone if you can't connect to Outlook. Setting it to zero would erase the last valid number.

Coding Challenge

At the end of some of my Coding4Fun columns, I will have a little coding challenge—something for you to work on if you are interested. For this article, the challenge is to create anything e-mail related, and it doesn't have to be POP3. Managed code is preferred (Visual Basic .NET, C#, J#, or Managed C++ please), but an unmanaged component that exposes a COM interface would also be good. Just post whatever you produce to GotDotNet and send me an e-mail message (at duncanma@microsoft.com) with an explanation of what you have done and why you feel it is interesting. You can send me your ideas whenever you like, but please just send me links to code samples, not the samples themselves (my inbox thanks you in advance).

Resources

I used four samples from GotDotNet when building the code for this article, and I am always scanning the list of user samples for useful and cool code. Here is a list of selected samples that you might find interesting:

Samples I Used in this Article

Other Interesting Samples

That last sample, the SmtpClient, is a great example of the other side of mail—sending messages. Note that System.Web.Mail provides Smtp support right in the .NET Framework but it is dependent on CDONTS, while that sample is pure sockets. I had not noticed before, but boss is so into C# that even his sample name has a semi-colon at the end!

Have your own ideas for hobbyist content? Let me know at duncanma@microsoft.com, and happy coding!

 

Coding4Fun

Duncan Mackenzie is the Microsoft Visual Basic .NET Content Strategist for MSDN during the day and a dedicated coder late at night. It has been suggested that he wouldn't be able to do any work at all without his Earl Grey tea, but let's hope we never have to find out. For more on Duncan, see his site.