Click here to Skip to main content
15,890,438 members
Articles / Web Development / ASP.NET
Article

A web based dialup Internet application

Rate me:
Please Sign up or sign in to vote.
4.91/5 (36 votes)
6 Jul 20032 min read 201.6K   3.6K   76   36
A web application to connect and disconnect from a dial up Internet session

Sample Image - webdialup.gif

Introduction

I recently changed my home network, basically I added a server to connect the network to the Internet. Unfortunately where I live we can't get broadband Internet, so the Internet connection server needs to dial into our ISP. I needed a simple way to connect, disconnect and see how long the connection has been up. Normally I would use terminal services to establish and view the connection, but since my wife also uses the Internet I needed another simpler way, one that even my wife can use. I built this web application, that displays the current connection's statistics or shows the phonebook entries so that the user can connect.

Using the code

I've wrapped up some of the RAS API's so I could use them with P/Invoke. They are:

  • RasEnumConnections
  • RasGetConnectionStatistics
  • RasHangUp
  • RasEnumEntries
  • InternetDial

I also had to create some structures that these API's could use:

  • RASCONN
  • RasEntryName
  • RasStats

I created a simple class called RASDisplay which has the following methods and properties:

Methods

  • int Connect(string Connection)
  • void Disconnect()

Properties

  • bool IsConnected
  • string ConnectionName
  • double BytesReceived
  • double BytesTransmitted
  • string[] Connections
  • string Duration

The RASDisplay class takes care of all the complexity of using the RAS API. If you not familiar with using the RAS API, you have to pass in the structure sizes, so the API knows which version you are working with. The constructor for this class uses the following code to set the above properties:

C#
private string m_duration;
private string m_ConnectionName;
private string[] m_ConnectionNames;
private double m_TX;
private double m_RX;
private bool m_connected;
private IntPtr m_ConnectedRasHandle;

public RASDisplay()
{
    m_connected = true;

    RAS lpras = new RAS();
    RASCONN lprasConn = new RASCONN();            

    lprasConn.dwSize = Marshal.SizeOf(typeof(RASCONN));
    lprasConn.hrasconn = IntPtr.Zero;

    int lpcb = 0;
    int lpcConnections = 0;
    int nRet = 0;
    lpcb = Marshal.SizeOf(typeof(RASCONN));


    nRet = RAS.RasEnumConnections(ref lprasConn, ref lpcb, 
                                         ref lpcConnections);


    if(nRet != 0)
    {
        m_connected = false;
        return;
    }

    if(lpcConnections > 0)
    {
        RasStats stats = new RasStats();

        m_ConnectedRasHandle = lprasConn.hrasconn;
        RAS.RasGetConnectionStatistics(lprasConn.hrasconn, stats);

        m_ConnectionName = lprasConn.szEntryName;

        //Work out our duration 
        int Hours = 0;
        int Minutes = 0;
        int Seconds = 0;

        //The RasStats duration is in milliseconds
        Hours = ((stats.dwConnectDuration /1000) /3600);
        Minutes = ((stats.dwConnectDuration /1000) /60) - 
                                              (Hours * 60);
        Seconds = ((stats.dwConnectDuration /1000)) - 
                           (Minutes * 60) - (Hours * 3600);

        m_duration = Hours  +  " hours "  + Minutes + 
                " minutes " + Seconds + " secs";

        //set the bytes transferred and received
        m_TX = stats.dwBytesXmited;
        m_RX = stats.dwBytesRcved;

    }
    else
    {
        //we aren't connected
        m_connected = false;
    }

    //Find the names of the connections we could dial
    int lpNames = 1;
    int entryNameSize=Marshal.SizeOf(typeof(RasEntryName));
    int lpSize=lpNames*entryNameSize;
    RasEntryName[] names=new RasEntryName[lpNames];
    for(int i=0;i<names.Length;i++)
    {
        names[i].dwSize=entryNameSize;
    }

    uint retval = RAS.RasEnumEntries(null,null,names,
                                 ref lpSize,out lpNames);

    m_ConnectionNames = new string[lpNames];

    if(lpNames>0)
    {                
        for(int i=0;i<lpNames;i++)
        {
            m_ConnectionNames[i] = names[i].szEntryName;
        }
    }
}

The code to connect to the Internet uses the InternetDial WinInet API:

C#
public int Connect(string sConnection)
{
    int intConnection = 0;
    uint INTERNET_AUTO_DIAL_UNATTENDED = 2;
    int retVal = RAS.InternetDial(IntPtr.Zero, sConnection,
            INTERNET_AUTO_DIAL_UNATTENDED,ref intConnection,0);
    return retVal;
}

The code to disconnect is very simple as well:

C#
public void Disconnect()
{
    RAS.RasHangUp(m_ConnectedRasHandle);
}

The web application that uses the RASDisplay class looks like:

C#
protected System.Web.UI.WebControls.Label lblName;
protected System.Web.UI.WebControls.Label lblDuration;
protected System.Web.UI.WebControls.Label lblTransmitted;
protected System.Web.UI.WebControls.DataGrid dgAllConnections;
protected System.Web.UI.HtmlControls.HtmlTable tblCurrentConnection;
protected System.Web.UI.WebControls.Button btnDisconnect;
protected System.Web.UI.WebControls.Label lblRecieved;

private void Page_Load(object sender, System.EventArgs e)
{
    if(!IsPostBack)
    {
        BindData();
    }
}

private void BindData()
{
    try
    {
        RASDisplay display = new RASDisplay();

        lblName.Text = display.ConnectionName;
        lblDuration.Text = display.Duration;
        lblRecieved.Text = display.BytesReceived.ToString();
        lblTransmitted.Text = display.BytesTransmitted.ToString();

        if(!display.IsConnected)
        {
            //show the table with stats
            tblCurrentConnection.Visible = false;
        }
        else
        {
            //show the tables with available connections
            dgAllConnections.Visible = false;
        }

        //bind the data
        dgAllConnections.DataSource = display.Connections;
        dgAllConnections.DataBind();

    }
    catch(Exception e){
        Response.Write(e.Message);
    }
}

protected void btnConnect_Click(object sender, EventArgs e)
{
    RASDisplay rasDisplay = new RASDisplay();

    //need to find the text in the first column of the datagrid
    Button btnSender = (Button) sender;
    DataGridItem ob =  (DataGridItem) btnSender.Parent.Parent;

    int ErrorVal = rasDisplay.Connect(ob.Cells[1].Text);

    if(ErrorVal != 0)
    {
        Response.Write(ErrorVal);
    }
    else
    {
        //redirect to the same page, so the display 
        //is refreshed with stats
        Response.Redirect("Default.aspx");
    }
}

protected void btnDisConnect_Click(object sender, EventArgs e)
{
    RASDisplay rasDisplay = new RASDisplay();
    rasDisplay.Disconnect();
    //redirect to the same page, so the display is 
    //refreshed with available connections
    Response.Redirect("Default.aspx");
}

The web application has a DataGrid that displays all the connections on the machine, this DataGrid only gets displayed if the computer isn't connected to the Internet. Otherwise a table with the connection's statistics gets displayed. The web page has a meta refresh tag, to keep the statistics up to date.

Know issues

Currently the Connect method calls InternetDial, this method takes the name of the connection. If this connection doesn't have a password saved, the call will fail. In a future version I'd like to change the way the application connects to the Internet, maybe using RASDial and have the application store the username and password for the user (or require them to enter it ? )

History

  • Article Created: 30/06/2003
  • Updated 7/7/2003 - Just a bug fix, if the user had more than one dialup internet phonebook, they might have some problems.

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
Australia Australia
I've been programming for a few years now. I blog regularly at httpcode.

Comments and Discussions

 
QuestionConnect return 87 Pin
sealion_012629-Jun-14 1:33
sealion_012629-Jun-14 1:33 
QuestionAlready have Internet connection but how to use RAS with that please help Pin
Umesh Deshmukh3522-Mar-12 1:09
Umesh Deshmukh3522-Mar-12 1:09 
GeneralYou Deserve a 5. Good stuff Pin
Ariel Delgado4-Nov-10 21:14
Ariel Delgado4-Nov-10 21:14 
GeneralVery Informative Pin
Mohammad Rastkar24-Jun-08 3:52
Mohammad Rastkar24-Jun-08 3:52 
GeneralDial up connnection in c#/.net Pin
satheesh.yuva13-Apr-08 21:13
satheesh.yuva13-Apr-08 21:13 
QuestionGreat Job - Need help translating it to VB.NET Pin
Member 44388863-Apr-08 11:11
Member 44388863-Apr-08 11:11 
When I translate the code it does not work when the call is made to RasEnumEntries Function:
Here is the code:
<br />
Imports System.Runtime.InteropServices<br />
Namespace Utilities.Dialup.RAS<br />
<br />
    Public Enum RasFieldSizeConstants<br />
        RAS_MaxDeviceType = 16<br />
        RAS_MaxPhoneNumber = 128<br />
        RAS_MaxIpAddress = 15<br />
        RAS_MaxIpxAddress = 21<br />
#If WINVER4 Then<br />
		RAS_MaxEntryName      =256<br />
		RAS_MaxDeviceName     =128<br />
		RAS_MaxCallbackNumber =RAS_MaxPhoneNumber<br />
#Else<br />
        RAS_MaxEntryName = 20<br />
        RAS_MaxDeviceName = 32<br />
        RAS_MaxCallbackNumber = 48<br />
#End If<br />
<br />
        RAS_MaxAreaCode = 10<br />
        RAS_MaxPadType = 32<br />
        RAS_MaxX25Address = 200<br />
        RAS_MaxFacilities = 200<br />
        RAS_MaxUserData = 200<br />
        RAS_MaxReplyMessage = 1024<br />
        RAS_MaxDnsSuffix = 256<br />
        UNLEN = 256<br />
        PWLEN = 256<br />
        DNLEN = 15<br />
    End Enum<br />
<br />
    <structlayout(layoutkind.sequential, charset:="CharSet.Auto)"> _<br />
    Public Structure GUID<br />
        Public Data1 As UInteger<br />
        Public Data2 As UShort<br />
        Public Data3 As UShort<br />
        <marshalas(unmanagedtype.byvalarray, sizeconst:="8)"> _<br />
        Public Data4() As Byte<br />
<br />
    End Structure<br />
<br />
    <structlayout(layoutkind.sequential, charset:="CharSet.Auto)"> _<br />
  Public Structure RASCONN<br />
        Public dwSize As Integer<br />
        Public hrasconn As IntPtr<br />
        <marshalas(unmanagedtype.byvaltstr, sizeconst:="CInt(RasFieldSizeConstants.RAS_MaxEntryName" +="" 1))=""> _<br />
        Public szEntryName As String<br />
        <marshalas(unmanagedtype.byvaltstr, sizeconst:="CInt(RasFieldSizeConstants.RAS_MaxDeviceType" +="" 1))=""> _<br />
        Public szDeviceType As String<br />
        <marshalas(unmanagedtype.byvaltstr, sizeconst:="CInt(RasFieldSizeConstants.RAS_MaxDeviceName" +="" 1))=""> _<br />
        Public szDeviceName As String<br />
        'MAX_PAPTH=260<br />
        <marshalas(unmanagedtype.byvaltstr, sizeconst:="260)"> _<br />
        Public szPhonebook As String<br />
        Public dwSubEntry As Integer<br />
        Public guidEntry As GUID<br />
#If (WINVER501) Then<br />
		Public   dwFlags AS integer<br />
		Public       luid AS LUID<br />
#End If<br />
    End Structure<br />
<br />
    <structlayout(layoutkind.sequential, charset:="CharSet.Auto)"> _<br />
    Public Structure LUID<br />
        Dim LowPart As Integer<br />
        Dim HighPart As Integer<br />
    End Structure<br />
<br />
    <structlayout(layoutkind.sequential, charset:="CharSet.Auto)"> _<br />
    Public Structure RasEntryName<br />
        Public dwSize As Integer<br />
        <marshalas(unmanagedtype.byvaltstr, sizeconst:="CInt(RasFieldSizeConstants.RAS_MaxEntryName)" +="" 1)=""> _<br />
        Public szEntryName As String<br />
#If WINVER5 Then<br />
        public dwFlags as Integer<br />
        <marshalas(unmanagedtype.byvaltstr,sizeconst:> _ <br />
        Public szPhonebookPath as String<br />
#End If<br />
    End Structure<br />
<br />
    <structlayout(layoutkind.sequential, charset:="CharSet.Auto)"> _<br />
    Public Class RasStats<br />
        Public dwSize As Integer = Marshal.SizeOf(GetType(RasStats))<br />
        Public dwBytesXmited As Integer<br />
        Public dwBytesRcved As Integer<br />
        Public dwFramesXmited As Integer<br />
        Public dwFramesRcved As Integer<br />
        Public dwCrcErr As Integer<br />
        Public dwTimeoutErr As Integer<br />
        Public dwAlignmentErr As Integer<br />
        Public dwHardwareOverrunErr As Integer<br />
        Public dwFramingErr As Integer<br />
        Public dwBufferOverrunErr As Integer<br />
        Public dwCompressionRatioIn As Integer<br />
        Public dwCompressionRatioOut As Integer<br />
        Public dwBps As Integer<br />
        Public dwConnectDuration As Integer<br />
    End Class<br />
<br />
    Public Class RAS<br />
<br />
        Sub New()<br />
<br />
        End Sub<br />
<br />
        <dllimport("rasapi32.dll", entrypoint:="RasEnumConnectionsA" ,="" setlasterror:="True)"> _<br />
        Public Shared Function RasEnumConnections( _<br />
        ByRef lprasconn As RASCONN _<br />
        , ByRef lpcb As Integer _<br />
        , ByRef lpcConnections As Integer _<br />
        ) As Integer<br />
<br />
        End Function<br />
<br />
<br />
<br />
        ''' <summary><br />
        ''' <br />
        ''' </summary><br />
        ''' <param name="hRasConn"> handle to the connection</param><br />
        ''' <param name="hRasConn"> buffer to receive statistics</param><br />
        ''' <returns></returns><br />
        ''' <remarks></remarks><br />
        <dllimport("rasapi32.dll", charset:="CharSet.Auto)"> _<br />
Public Shared Function RasGetConnectionStatistics( _<br />
        ByVal hRasConn As IntPtr _<br />
 , ByRef lpStatistics As RasStats _<br />
 ) As UInteger<br />
<br />
        End Function<br />
<br />
        ''' <summary><br />
        ''' <br />
        ''' </summary><br />
        ''' <param name="hrasconn">handle to the RAS connection to hang up</param><br />
        ''' <returns></returns><br />
        ''' <remarks></remarks><br />
        <dllimport("rasapi32.dll", charset:="CharSet.Auto)"> _<br />
        Public Shared Function RasHangUp( _<br />
        ByVal hrasconn As IntPtr _<br />
         ) As UInteger<br />
<br />
        End Function<br />
<br />
        ''' <summary><br />
        ''' <br />
        ''' </summary><br />
        ''' <param name="reserved">reserved, must be NULL</param><br />
        ''' <param name="lpszPhonebook">pointer to full path and file name of phone-book file</param><br />
        ''' <param name="lprasentryname">buffer to receive phone-book entries</param><br />
        ''' <param name="lpcb">size in bytes of buffer</param><br />
        ''' <param name="lpcEntries">number of entries written to buffer</param><br />
        ''' <returns></returns><br />
        ''' <remarks></remarks><br />
        <dllimport("rasapi32.dll", charset:="CharSet.Auto)"> _<br />
        Public Shared Function RasEnumEntries( _<br />
        ByVal reserved As String, _<br />
        ByVal lpszPhonebook As String, _<br />
         ByRef lprasentryname As RasEntryName(), _<br />
         ByRef lpcb As Integer, _<br />
         ByRef lpcEntries As Integer _<br />
         ) As UInteger<br />
        End Function<br />
</dllimport("rasapi32.dll",><br />
<br />
<br />
        <dllimport("wininet.dll", charset:="CharSet.Auto)"> _<br />
        Public Shared Function InternetDial( _<br />
        ByVal hwnd As IntPtr, _<br />
        ByVal lpszConnectoid As String, _<br />
        ByVal dwFlags As UInteger, _<br />
         ByRef lpdwConnection As Integer, _<br />
        ByVal dwReserved As UInteger _<br />
         ) As Integer<br />
        End Function<br />
<br />
    End Class<br />
<br />
    Public Class RASDisplay<br />
        Private m_duration As String<br />
        Private m_ConnectionName As String<br />
        Private m_ConnectionNames() As String<br />
        Private m_TX As Double<br />
        Private m_RX As Double<br />
        Private m_connected As Boolean<br />
        Private m_ConnectedRasHandle As IntPtr<br />
<br />
        Sub New()<br />
            m_connected = True<br />
<br />
            Dim lpras As RAS = New RAS<br />
            Dim lprasConn As RASCONN = New RASCONN<br />
<br />
            lprasConn.dwSize = Marshal.SizeOf(GetType(RASCONN))<br />
            lprasConn.hrasconn = IntPtr.Zero<br />
<br />
            Dim lpcb As Integer = 0<br />
            Dim lpcConnections As Integer = 0<br />
            Dim nRet As Integer = 0<br />
            lpcb = Marshal.SizeOf(GetType(RASCONN))<br />
<br />
            nRet = RAS.RasEnumConnections(lprasConn, lpcb, lpcConnections)<br />
<br />
            If nRet <> 0 Then<br />
                m_connected = False<br />
                Return<br />
            End If<br />
<br />
            If lpcConnections > 0 Then<br />
<br />
                Dim stats As RasStats = New RasStats<br />
                m_ConnectedRasHandle = lprasConn.hrasconn<br />
                RAS.RasGetConnectionStatistics(lprasConn.hrasconn, stats)<br />
<br />
                m_ConnectionName = lprasConn.szEntryName<br />
<br />
                Dim Hours As Integer = 0<br />
                Dim Minutes As Integer = 0<br />
                Dim Seconds As Integer = 0<br />
<br />
                Hours = ((stats.dwConnectDuration / 1000) / 3600)<br />
                Minutes = ((stats.dwConnectDuration / 1000) / 60) - (Hours * 60)<br />
                Seconds = ((stats.dwConnectDuration / 1000)) - (Minutes * 60) - (Hours * 3600)<br />
<br />
                m_duration = Hours + " hours " + Minutes + " minutes " + Seconds + " secs"<br />
                m_TX = stats.dwBytesXmited<br />
                m_RX = stats.dwBytesRcved<br />
<br />
            Else<br />
                m_connected = False<br />
            End If<br />
<br />
            Dim lpNames As Integer = 1<br />
            Dim entryNameSize As Integer = 0<br />
            Dim lpSize As Integer = 0<br />
            Dim names() As RasEntryName<br />
<br />
            entryNameSize = Marshal.SizeOf(GetType(RasEntryName))<br />
            lpSize = lpNames * entryNameSize<br />
<br />
            ReDim names(lpNames - 1)<br />
            names(0).dwSize = entryNameSize<br />
<br />
            Dim retval As UInteger = RAS.RasEnumEntries(Nothing, Nothing, names, lpSize, lpNames)<br />
<br />
            ' if we have more than one connection, we need to do it again<br />
            If lpNames > 1 Then<br />
                ReDim names(lpNames)<br />
                For i As Integer = 0 To names.Length - 1<br />
                    names(0).dwSize = entryNameSize<br />
                Next<br />
                retval = RAS.RasEnumEntries(Nothing, Nothing, names, lpSize, lpNames)<br />
<br />
            End If<br />
            ReDim m_ConnectionNames(names.Length)<br />
<br />
            If lpNames > 0 Then<br />
                For i As Integer = 0 To names.Length - 1<br />
                    m_ConnectionNames(i) = names(i).szEntryName<br />
                Next<br />
            End If<br />
        End Sub<br />
<br />
<br />
        Public ReadOnly Property Duration() As String<br />
            Get<br />
                Return IIf(m_connected, m_duration, "")<br />
            End Get<br />
        End Property<br />
<br />
        Public ReadOnly Property Connections() As String()<br />
            Get<br />
                Return m_ConnectionNames<br />
            End Get<br />
        End Property<br />
<br />
        Public ReadOnly Property BytesTransmitted() As Double<br />
            Get<br />
                Return IIf(m_connected, m_TX, 0)<br />
            End Get<br />
        End Property<br />
<br />
        Public ReadOnly Property BytesReceived() As Double<br />
            Get<br />
                Return IIf(m_connected, m_RX, 0)<br />
            End Get<br />
        End Property<br />
<br />
        Public ReadOnly Property ConnectionName() As String<br />
            Get<br />
                Return IIf(m_connected, m_ConnectionName, "")<br />
            End Get<br />
        End Property<br />
<br />
        Public ReadOnly Property IsConnected() As Boolean<br />
            Get<br />
                Return m_connected<br />
            End Get<br />
        End Property<br />
<br />
        Public Function Connect(ByVal connection As String) As Integer<br />
            Dim temp As Integer = 0<br />
            Dim INTERNET_AUTO_DIAL_UNATTENDED As UInteger = 2<br />
            Dim retVal As Integer = RAS.InternetDial(IntPtr.Zero, connection, INTERNET_AUTO_DIAL_UNATTENDED, temp, 0)<br />
            Return retVal<br />
        End Function<br />
<br />
        Public Sub Disonnect(ByVal connection As String)<br />
            RAS.RasHangUp(m_ConnectedRasHandle)<br />
        End Sub<br />
<br />
    End Class<br />
<br />
<br />
End Namespace<br />
</dllimport("wininet.dll",></dllimport("rasapi32.dll",></dllimport("rasapi32.dll",></dllimport("rasapi32.dll",></structlayout(layoutkind.sequential,></marshalas(unmanagedtype.byvaltstr,sizeconst:></marshalas(unmanagedtype.byvaltstr,></structlayout(layoutkind.sequential,></structlayout(layoutkind.sequential,></marshalas(unmanagedtype.byvaltstr,></marshalas(unmanagedtype.byvaltstr,></marshalas(unmanagedtype.byvaltstr,></marshalas(unmanagedtype.byvaltstr,></structlayout(layoutkind.sequential,></marshalas(unmanagedtype.byvalarray,></structlayout(layoutkind.sequential,>

Appreciate any help posible Dead | X|
GeneralYou got 5 Pin
David Bazan26-Feb-08 9:00
David Bazan26-Feb-08 9:00 
GeneralCant disconnect neither get ANY exception Pin
Muammar©16-Mar-07 2:30
Muammar©16-Mar-07 2:30 
GeneralProblem in Getting Duration Pin
ss_hellhound16-Jan-06 6:06
ss_hellhound16-Jan-06 6:06 
GeneralI LOVE YOU Pin
naiemk21-Oct-05 20:02
naiemk21-Oct-05 20:02 
GeneralQuestion about Improvement of RASDisplay class Pin
Jonny Depp10-Aug-05 22:27
Jonny Depp10-Aug-05 22:27 
GeneralError 691 Pin
sgomcres8-Sep-04 23:06
sgomcres8-Sep-04 23:06 
GeneralRe: Error 691 Pin
Ahmad Maatouki9-Jan-07 10:50
Ahmad Maatouki9-Jan-07 10:50 
GeneralNeed help! Pin
Rockman X724-Aug-04 23:42
Rockman X724-Aug-04 23:42 
GeneralHook to dos window Pin
frank5976-Aug-04 12:35
frank5976-Aug-04 12:35 
GeneralUse of same Pin
daniel doran10-Jun-04 5:27
daniel doran10-Jun-04 5:27 
GeneralRasEnumConnections Error Pin
bsargos31-Jan-04 9:44
bsargos31-Jan-04 9:44 
GeneralRe: RasEnumConnections Error Pin
Member 267675518-Apr-08 2:07
Member 267675518-Apr-08 2:07 
QuestionVery well but problem with win98? Pin
ArminK30-Jan-04 3:11
ArminK30-Jan-04 3:11 
GeneralGood job. Two questions : Pin
bsargos29-Jan-04 13:47
bsargos29-Jan-04 13:47 
GeneralNeed a small help.... Pin
Member 7339988-Dec-03 0:17
Member 7339988-Dec-03 0:17 
GeneralDial-UP connectio Pin
Member 7400547-Dec-03 12:48
Member 7400547-Dec-03 12:48 
GeneralRe: Dial-UP connectio Pin
benoityip8-Dec-03 2:33
benoityip8-Dec-03 2:33 
Questiongot this error...any ideas??? Pin
avla25-Nov-03 23:18
avla25-Nov-03 23:18 
GeneralCRITICAL error when reusing object : no disconnect Pin
Ups10116-Oct-03 6:31
Ups10116-Oct-03 6:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.