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!

2 comments:

Justin Miller said...

This is great! Thanks for the post.

Dan Burns said...

Hello sir, trying to get this to work for me but it doesn't want to go. Keeps saying the 'using' keyword is not supported in this version of the language. This cmdlet would save my life in a big way right now, need SMS for notifications at my company desperately.

If you can help, it would be tremendously appreciated!