Friday, July 17, 2009

PowerShell and Google Voice

After a long wait, I finally got my invitation to Google Voice this week. I haven't completely converted to it, but it looks very useful. My brother got his Google Voice number about a week earlier, and he posted a Python script on his blog that sends an SMS using his account.

I decided to create a PowerShell Cmdlet that does the same thing. It is mostly just a translation of my brother's script, but it includes code from my Send-Email Cmdlet that uses a Gmail account.

Here is the code:

using System;

using System.Text;

using System.Management.Automation;

using System.Net;

using System.IO;

using System.Web;

using System.Text.RegularExpressions;

using System.Runtime.InteropServices;

using System.Security;

 

namespace CustomCmdlets

{

    [Cmdlet( VerbsCommunications.Send, "SMS", SupportsShouldProcess = true )]

    public class SendSMS : PSCmdlet

    {

        private const string GOOGLE_VOICE_ADDRESS = "https://www.google.com/voice/";

        private const string GOOGLE_VOICE_AUTHENTICATION_ADDRESS = "https://www.google.com/accounts/ServiceLoginAuth";

        private const string GOOGLE_VOICE_SMS_ADDRESS = "https://www.google.com/voice/sms/send/";

        private CookieCollection cookieCollection;

 

        #region Parameters

 

        private string googleUsername;

 

        [ValidateNotNullOrEmpty]

        public string GoogleUsername

        {

            get

            {

                return googleUsername;

            }

            set

            {

                googleUsername = value;

            }

        }

 

        private SecureString googlePassword;

 

        [ValidateNotNullOrEmpty]

        public SecureString GooglePassword

        {

            get

            {

                return googlePassword;

            }

            set

            {

                googlePassword = value;

            }

        }

 

        private string number;

 

        [Parameter( Position = 0, Mandatory = true )]

        [ValidateNotNullOrEmpty]

        public string Number

        {

            get

            {

                return number;

            }

            set

            {

                number = value;

            }

        }

 

        private string text;

 

        [Parameter( Position = 1, Mandatory = true )]

        public string Text

        {

            get

            {

                return text;

            }

            set

            {

                text = value;

            }

        }

 

        #endregion

 

        protected override void BeginProcessing()

        {

            if ( googleUsername == null )

            {

                googleUsername = (string)GetVariableValue( "GoogleUsername", null );

            }

 

            if ( googlePassword == null )

            {

                googlePassword = (SecureString)GetVariableValue( "GooglePassword", null );

            }

 

            if ( string.IsNullOrEmpty( googleUsername ) || googlePassword == null )

            {

                this.WriteError( new ErrorRecord(

                    new Exception( "You must provide a username and password." ),

                    "Send-SMS", ErrorCategory.PermissionDenied, this ) );

            }

        }

 

        protected override void EndProcessing()

        {

            IntPtr bstr = Marshal.SecureStringToBSTR( googlePassword );

            string plainGmailPassword = Marshal.PtrToStringAuto( bstr );

            Marshal.ZeroFreeBSTR( bstr );

 

            string postDataString = "&continue=" + HttpUtility.UrlEncode( GOOGLE_VOICE_ADDRESS ) +

                                    "&Email=" + HttpUtility.UrlEncode( googleUsername ) +

                                    "&Passwd=" + HttpUtility.UrlEncode( plainGmailPassword );

 

            this.cookieCollection = new CookieCollection();

 

            this.WriteVerbose( "Sending Google Voice login request..." );

 

            MakeHttpWebRequest( GOOGLE_VOICE_AUTHENTICATION_ADDRESS, true, Encoding.UTF8.GetBytes( postDataString ) );

 

            // if we don't have this cookie, something went wrong

            if ( this.cookieCollection[ "GAUSR" ] == null )

            {

                this.WriteError( new ErrorRecord(

                    new Exception( "Could not log in to Gmail.  Please check your username and password." ),

                    "Send-SMS", ErrorCategory.PermissionDenied, this ) );

            }

            else

            {

                this.WriteVerbose( "Sending Google Voice home page request..." );

 

                string googleVoiceHomePage = MakeHttpWebRequest( GOOGLE_VOICE_ADDRESS, false, null );

 

                Match rnrSeMatch = Regex.Match( googleVoiceHomePage, @"name=""_rnr_se"".*?value=""(?<Value>[^""]+)""" );

 

                if ( rnrSeMatch.Success )

                {

                    postDataString = "&_rnr_se=" + HttpUtility.UrlEncode( rnrSeMatch.Groups[ "Value" ].Value ) +

                                    "&phoneNumber=" + HttpUtility.UrlEncode( number ) +

                                    "&text=" + HttpUtility.UrlEncode( text );

 

                    this.WriteVerbose( "Sending Google Voice SMS request..." );

 

                    string sendResult = MakeHttpWebRequest( GOOGLE_VOICE_SMS_ADDRESS, true, Encoding.UTF8.GetBytes( postDataString ) );

 

                    Match resultMatch = Regex.Match( sendResult, @"\{""ok"":(?<Success>[^,]+),""data"":\{""code"":(?<Code>\d+)\}\}" );

 

                    if ( resultMatch.Success )

                    {

                        if ( bool.Parse( resultMatch.Groups[ "Success" ].Value ) )

                        {

                            this.WriteObject( "Your SMS has been sent." );

                        }

                        else

                        {

                            this.WriteError( new ErrorRecord(

                                new Exception( "Your SMS was not sent." ),

                                "Send-SMS", ErrorCategory.NotSpecified, this ) );

                        }

                    }

                }

            }

        }

 

        private string MakeHttpWebRequest( string requestUrl, bool post, byte[] postData )

        {

            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create( new Uri( requestUrl ) );

 

            // we need to do this ourselves

            webRequest.AllowAutoRedirect = false;

            webRequest.KeepAlive = false;

            webRequest.Credentials = CredentialCache.DefaultNetworkCredentials;

 

            webRequest.CookieContainer = new CookieContainer();

            webRequest.CookieContainer.Add( this.cookieCollection );

 

            if ( post )

            {

                webRequest.Method = "POST";

                webRequest.ContentType = "application/x-www-form-urlencoded";

 

                webRequest.ContentLength = postData.Length;

 

                Stream requestStream = null;

 

                try

                {

                    requestStream = webRequest.GetRequestStream();

                    requestStream.Write( postData, 0, postData.Length );

                }

                catch ( Exception ex )

                {

                    this.WriteError( new ErrorRecord( ex, "Send-SMS", ErrorCategory.InvalidData, this ) );

                }

                finally

                {

                    if ( requestStream != null )

                    {

                        requestStream.Close();

                    }

                }

            }

            else

            {

                webRequest.Method = "GET";

                webRequest.ContentType = "text/html";

            }

 

            HttpWebResponse webResponse = null;

            string responseString = "";

 

            try

            {

                webResponse = (HttpWebResponse)webRequest.GetResponse();

 

                cookieCollection.Add( webResponse.Cookies );

 

                StreamReader streamReader = new StreamReader( webResponse.GetResponseStream() );

 

                responseString = streamReader.ReadToEnd();

 

                streamReader.Close();

 

                // redirect if we have a Location header

                if ( webResponse.Headers[ "Location" ] != null )

                {

                    responseString = MakeHttpWebRequest( webResponse.Headers[ "Location" ], false, null );

                }

            }

            catch ( Exception ex )

            {

                this.WriteError( new ErrorRecord( ex, "Send-SMS", ErrorCategory.InvalidResult, this ) );

            }

            finally

            {

                if ( webResponse != null )

                {

                    webResponse.Close();

                }

            }

 

            return responseString;

        }

    }

}


This Cmdlet can use the Init-Gmail PowerShell function (renamed Init-Google) I wrote for the Send-Email Cmdlet, as well as a shortcut hash I created to make sending texts easier:

function Init-Google
{
    [string]$global:GoogleUsername = Read-Host "Google username"
    [System.Security.SecureString]$global:GooglePassword = Read-Host "Google password" -AsSecureString 
}

$sms = @{ "Jeff"    = "8015551234";
          "Mindy"   = "8015552345";
          "Dad"     = "8015553456";
          "Jason"   = "8015554567";
          "Scott"   = "8015555678";
          "Nate"    = "8015556789";
          "Branden" = "8015557890" }

Using the Cmdlet is very easy:

C:\
PSH$ send-sms $sms.jeff "PowerShell + Google = Awesome."
Your SMS has been sent.

This is another one that I did just for fun, but it is already turning out to be pretty useful. I can type using a real keyboard a lot faster than I can on any phone, and not having to pull my phone out of my pocket to send a quick text to my wife is very handy.

Enjoy!