Friday, October 17, 2008

PowerShell and StackOverflow.com

Not long ago, I created an account on StackOverflow.com, Jeff Attwood and Joel Spolsky's new question and answer site for programmers. Asking a good question or providing a good answer will result in the other members of the communtity responding by voting for your question or answer. Users can also vote down questions or answers they don't think are useful or correct. Each positive vote increases the user's reputation, which is used to determine how much a user is "trusted" on the site. More "trust" allows the user to have more control over what is on the site; it determines what aspects of the site they can edit, wiki-style. Currently, I only have enough reputation to edit the tags associated with a question, but it is possible for a user to have enough reputation that they can edit other user's questions and even close them.
Something that was missing from the site when I signed up was the ability to figure out where your reputation points were coming from. If you had several questions or answers, it was hard to keep track of which ones were contributing to your reputation score. Another user of the site noticed this deficiency before I did, and created a Python script that will tell you what has changed with your account since the last time your ran the script. The script requires Python and SQLite. This seemed like something could be done pretty easily using PowerShell, so I put together a similar script that does pretty much the same thing:


param ( [int] $userNumber = 3950 )

[Reflection.Assembly]::LoadWithPartialName( "System.Web" ) | Out-Null

$webClient = New-Object Net.WebClient

$profile = $webClient.DownloadString( 
    "http://stackoverflow.com/users/$userNumber/myProfile.html" )

$userRegex = 'User (?<User>.*?) - Stack Overflow'
$user = [regex]::Match( $profile, $userRegex ).Groups[ "User" ].Value

$reputationRegex = '<div[^>]+class="summarycount">' + 
                   '[^\d]+(?<Reputation>[,\d]+)</div>' + 
                   '[^<]*<div[^>]+>Reputation'

$reputation = [regex]::Match( 
    $profile, $reputationRegex ).Groups[ "Reputation" ].Value -replace ","

$badgeRegex = '<div[^>]+class="summarycount"[^>]*>' + 
              '[^\d]+(?<Badges>\d+)</div>[^<]*</td>' + 
              '[^<]*<td[^>]*>[^<]*<h1[^>]*>Badges'

$badges = [regex]::Match( 
    $profile, $badgeRegex ).Groups[ "Badges" ].Value -replace ","

$profileXmlPath = Join-Path ( Get-Location ) "Profile_$userNumber.xml"

if ( !Test-Path $profileXmlPath ) )
{
    Out-File -FilePath $profileXmlPath -InputObject @"
<Profile>
  <User>$user</User>
  <Reputation>$reputation</Reputation>
  <Badges>$badges</Badges>
  <Questions />
</Profile>    
"@
}

[xml]$profileXml = Get-Content $profileXmlPath

$existingQuestions = $profileXml.Profile.Questions.Clone()

if ( $profileXml.Profile[ "Questions" ].Question )
{
    $profileXml.Profile.Questions.RemoveAll()
}

function Process-Questions( [string] $questionRegex, [string] $questionType )
{
    $questionChangeHash = @{}

    foreach ( $questionMatch in [regex]::Matches( 
        $profile, $questionRegex, 'SingleLine' ) )
    {
        $id = $questionMatch.Groups[ "ID" ].Value
        $question = $questionMatch.Groups[ "Question" ].Value
        $votes = $questionMatch.Groups[ "Votes" ].Value

        $questionNode = $existingQuestions.Question | 
            Where-Object { $_.ID -eq $id }

        if ( $questionNode )
        {
            if ( [int]$votes -gt $questionNode.Votes )
            {
                $questionChangeHash$id ] = 
                    "+$( [int]$votes - $questionNode.Votes )"
            }
            elseif ( [int]$votes -lt $questionNode.Votes )
            {
                $questionChangeHash$id ] = [int]$votes - $questionNode.Votes
            }

            $questionNode.Votes = $votes

            $profileXml.Profile[ "Questions" ].AppendChild( 
                $questionNode ) | Out-Null
        }
        else
        {
            & {
                $script:questionNode = $profileXml.CreateElement( "Question" )

                $script:questionNode.AppendChild( 
                    $profileXml.CreateElement( "ID" ) )
                $script:questionNode.ID = $id

                $script:questionNode.AppendChild( 
                    $profileXml.CreateElement( "Type" ) )
                $script:questionNode.Type = $questionType

                $script:questionNode.AppendChild( 
                    $profileXml.CreateElement( "Question" ) )
                $script:questionNode.Question = $question

                $script:questionNode.AppendChild( 
                    $profileXml.CreateElement( "Votes" ) )
                $script:questionNode.Votes = $votes

                $profileXml.Profile[ "Questions" ].AppendChild( 
                    $script:questionNode )
            } | Out-Null

            $questionChangeHash$id ] = "(New)"
        }
    }

    $screenWidth = $Host.UI.RawUI.WindowSize.Width
    $elipsis = "..."

    if ( $questionChangeHash.Keys.Count -gt 0 )
    {
        $profileXml.Profile.Questions.Question | 
            Where-Object { $_.Type -eq $questionType } | 
            Select-Object `
                @{ Name = "$( $questionType )s"
                   Expression = {
                      $question = 
                        [System.Web.HttpUtility]::HtmlDecode( $_.Question ) 
                      $questionMaxLength = 
                        $screenWidth - ( " Votes Change ".Length )

                      if ( $question.Length -gt $questionMaxLength )
                      {
                          $question = "{0}$elipsis" -f 
                              $question.SubString( 
                                0, $questionMaxLength - $elipsis.Length ) 
                      }

                      $question
                   } 
                }, 
                Votes, 
                @{ Name = "Change"
                   Expression = {
                       $questionChangeHash$_.ID ] 
                   } 
                } | Format-Table -AutoSize
    }
    else
    {
        $profileXml.Profile.Questions.Question | 
            Where-Object { $_.Type -eq $questionType } | 
            Select-Object `
                @{ Name = "$( $questionType )s"
                   Expression = { 
                      $question = 
                        [System.Web.HttpUtility]::HtmlDecode( $_.Question ) 
                      $questionMaxLength = 
                        $screenWidth - ( " Votes ".Length )

                      if ( $question.Length -gt $questionMaxLength )
                      {
                          $question = "{0}$elipsis" -f 
                              $question.SubString( 
                                0, $questionMaxLength - $elipsis.Length ) 
                      }

                      $question
                   } 
                }, 
                Votes | Format-Table -AutoSize
    }    
}

$questionRegex = '<div class="question-summary narrow"[^>]+>' + 
                 '.*?<div class="mini-counts">(?<Votes>[\d,-]+).*?' + 
                 '<div class="summary">[^<]*<h3>[^<]*' + 
                 '<a\s*href="/questions/(?<ID>\d+)[^>]+>(?<Question>[^<]+)'

Process-Questions $questionRegex "Question"

$answerRegex = '<div class="answer-summary">' + 
               '<a[^>]+><div class="answer-votes[^>]+>(?<Votes>[\d,-]+)' +
               '</div></a><div class="answer-link">' + 
               '<a\s*href="/questions/(?<ID>\d+)[^>]+>(?<Question>[^<]+)'

Process-Questions $answerRegex "Answer"

$reputationChange = ""
$badgeChange = ""

if ( [int]$reputation -gt $profileXml.Profile.Reputation )
{
    $reputationChange = 
        "+$( [int]$reputation - $profileXml.Profile.Reputation )"
}
elseif ( [int]$reputation -lt $profileXml.Profile.Reputation )
{
    $reputationChange = $reputation - $profileXml.Profile.Reputation
}

if ( [int]$badges -gt $profileXml.Profile.Badges )
{
    $badgeChange = "+$( [int]$badges - $profileXml.Profile.Badges )"
}
elseif ( [int]$badges -lt $profileXml.Profile.Badges )
{
    $badgeChange = [int]$badges - $profileXml.Profile.Badges
}

$profileXml.Profile.User = $user
$profileXml.Profile.Reputation = $reputation
$profileXml.Profile.Badges = $badges

$profileXml.Profile | 
    Select-Object User,
                  @{ Name = "Reputation"
                     Expression = {
                         if ( $reputationChange )
                         {
                             "{0} ({1})" -f 
                                $_.Reputation, $reputationChange 
                         }
                         else
                         {
                             $_.Reputation
                         }
                     } 
                  },
                  @{ Name = "Badges"
                     Expression = {
                         if ( $badgeChange )
                         {
                             "{0} ({1})" -f $_.Badges, $badgeChange 
                         }
                         else
                         {
                             $_.Badges
                         }
                     } 
                  } | Format-List

$profileXml.Save( $profileXmlPath )


The results are displayed in the typical PowerShell way:

PSH$ .\Get-StackOverflowReputation.ps1 3950

Answers Votes
------- -----
What is the one programming skill you have always wanted to master but haven't had time? 30
Is there a meaningful correlation between spelling and programming ability? 6
Factorial Algorithms in different languages 6
Modal popups - usability 6
How can I uninstall an application using PowerShell? 5
Function pointers in C - address operator "unnecessary" 5
Getting developers fired up about development 5
Rule you know you should follow but don't 4
"Hidden Secrets" of the Visual Studio .NET debugger? 4
C++ Restrict Template Function 4
Should a novice programmer spend time learning to write "desktop" applications these days,... 4
Parsing a log file with regular expressions 3
Shortcut for commenting CSS in VS 2008 3
How do I perform string operations on variables in a for loop? 2
Expose an event handler to VBScript users of my COM object 2
Where do "pure virtual function call" crashes come from? 2
Why do C# and VB have Generics? What benefit do they provide? Generics, FTW 2
regular expression to replace two (or more) consecutive characters by only one? 2
Anyone using a third-party Windows registry editor that they would recommend to others? 2
Transparent form on the desktop 1
Test if a Font is installed 1
Using what I've learned from stackoverflow. (HTML Scraper) 1
How to detect the presence of a default recording device in the system? 1
How do I add Debug Breakpoints to lines displayed in a "Find Results" window in Visual Studio 0
Regex Question - One or more spaces outside of a quote enclosed block of text 0




User : Jeff Hillman
Reputation : 790
Badges : 9


StackOverflow.com has recently added a feature on the user account page that provides some information about how the user's reputation has changed, but it still doesn't give you the detail that these scripts provide.

This is just another example to me of how easy PowerShell can make tasks like this. This isn't likely to be useful for a long time, but it was fun. PowerShell is fun.

Friday, May 16, 2008

Custom Cmdlets - Part three

For this last post on the Cmdlets I have written, I will talk about my Send-Email Cmdlet. There are all kinds of scripts out there that show you how to send an email using the System.Net.Mail.SmtpClient class, and PowerShell Community Extensions also has a Cmdlet that will send an email for you using an SMTP server.

I am not a system administrator, so I don't have access to an SMTP server at the office where I currently work. I still wanted to be able to send an email from the command line or from a PowerShell script, so I added the ability to send an email using a Gmail account. The ability to use an SMTP server is still there, but it isn't nearly as interesting, so I won't talk about it here.

When I first decided I wanted to send an email using my Gmail account, I figured I would just find a .NET class library similar to the libraries provided for Blogger and some of the other Google services. I quickly discovered that not only did Google not provide an API for Gmail, but the libraries out there (that I could find, anyway) only provided the ability to read messages, not the ability to send them. After a little more research, I found a PHP library called libgmailer that could send messages. I studied the PHP code carefully, and translated just the parts I needed to log in to Gmail and send a message.

The most difficult part was handling attachments. Libgmailer uses the PHP/CURL class, which takes care of the messy stuff for you. I didn't know much about content types or really much else about HTTP requests (I'm not a web developer), so it was all pretty mysterious to me. I figured it out, and I am quite happy with the results:

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;

using System.Management.Automation;

using System.Net.Mail;

using System.Net;

using System.IO;

using System.Web;

using Microsoft.Win32;

using System.Text.RegularExpressions;

using System.Runtime.InteropServices;

using System.Security;

 

namespace CustomCmdlets

{

    [Cmdlet( VerbsCommunications.Send, "Email", SupportsShouldProcess = true, DefaultParameterSetName = "SMTP" )]

    public class SendEmail : PSCmdlet

    {

        private const int DefaultSmtpPort = 25;

 

        private const string GMAIL_ADDRESS = "https://mail.google.com/mail/";

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

        private CookieCollection cookieCollection;

        private string multiPartBoundary;

 

        private SmtpClient smtpClient;

        private List<FileInfo> inputAttachments;

 

        #region Parameters

 

        private SwitchParameter gmail;

 

        [Parameter( ParameterSetName = "Gmail" )]

        public SwitchParameter Gmail

        {

            get

            {

                return gmail;

            }

            set

            {

                gmail = value;

            }

        }

 

        private string gmailUsername;

 

        [Parameter( ParameterSetName = "Gmail", Mandatory = true )]

        [ValidateNotNullOrEmpty]

        public string GmailUsername

        {

            get

            {

                return gmailUsername;

            }

            set

            {

                gmailUsername = value;

            }

        }

 

        private SecureString gmailPassword;

 

        [Parameter( ParameterSetName = "Gmail", Mandatory = true )]

        [ValidateNotNullOrEmpty]

        public SecureString GmailPassword

        {

            get

            {

                return gmailPassword;

            }

            set

            {

                gmailPassword = value;

            }

        }

 

        private string smtpHost;

 

        [Parameter( ParameterSetName = "SMTP" )]

        [ValidateNotNullOrEmpty]

        public string SmtpHost

        {

            get

            {

                return smtpHost;

            }

            set

            {

                smtpHost = value;

            }

        }

 

        [Parameter( ParameterSetName = "SMTP" )]

        public int PortNumber

        {

            get

            {

                return portNumber ?? DefaultSmtpPort;

            }

            set

            {

                portNumber = value;

            }

        }

 

        private int? portNumber;

 

        private string from;

 

        [Parameter( ParameterSetName = "SMTP" )]

        [ValidateNotNullOrEmpty]

        public string From

        {

            get

            {

                return from;

            }

            set

            {

                from = value;

            }

        }

 

        private string[] toArray;

 

        [Parameter( Mandatory = true )]

        [ValidateNotNullOrEmpty]

        public string[] To

        {

            get

            {

                return toArray;

            }

            set

            {

                toArray = value;

            }

        }

 

        private string[] ccArray = new string[ 0 ];

 

        [Parameter]

        [ValidateNotNull]

        public string[] Cc

        {

            get

            {

                return ccArray;

            }

            set

            {

                ccArray = value;

            }

        }

 

        private string[] bccArray = new string[ 0 ];

 

        [Parameter]

        [ValidateNotNull]

        public string[] Bcc

        {

            get

            {

                return bccArray;

            }

            set

            {

                bccArray = value;

            }

        }

 

        private string subject;

 

        [Parameter( Mandatory = true )]

        public string Subject

        {

            get

            {

                return subject;

            }

            set

            {

                subject = value;

            }

        }

 

        private string body;

 

        [Parameter( Mandatory = true )]

        public string Body

        {

            get

            {

                return body;

            }

            set

            {

                body = value;

            }

        }

 

        private SwitchParameter bodyIsHtml;

 

        [Parameter]

        public SwitchParameter BodyIsHtml

        {

            get

            {

                return bodyIsHtml;

            }

            set

            {

                bodyIsHtml = value;

            }

        }

 

        private FileInfo[] attachments = new FileInfo[ 0 ];

 

        [Parameter( ValueFromPipeline = true )]

        [ValidateNotNull]

        public FileInfo[] Attachment

        {

            get

            {

                return attachments;

            }

            set

            {

                attachments = value;

            }

        }

 

        private int timeout = 60 * 1000;

 

        [Parameter]

        [ValidateRange( 0, Int32.MaxValue )]

        public int Timeout

        {

            get

            {

                return timeout;

            }

            set

            {

                timeout = value;

            }

        }

 

        #endregion

 

        protected override void BeginProcessing()

        {

            inputAttachments = new List<FileInfo>();

 

            if ( gmail.IsPresent )

            {

                if ( gmailUsername == null )

                {

                    gmailUsername = (string)GetVariableValue( "GmailUsername", null );

                }

 

                if ( gmailPassword == null )

                {

                    gmailPassword = (SecureString)GetVariableValue( "GmailPassword", null );

                }

 

                if ( string.IsNullOrEmpty( gmailUsername ) || gmailPassword == null )

                {

                    this.WriteError( new ErrorRecord(

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

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

                }

            }

            else

            {

                smtpHost = (string)GetVariableValue( "EmailSmtpHost" );

                portNumber = (int?)( GetVariableValue( "EmailSmtpPort" ) ?? DefaultSmtpPort );

                from = (string)GetVariableValue( "EmailFrom" );

 

                smtpClient = new SmtpClient( smtpHost );

                smtpClient.Port = portNumber.Value;

                smtpClient.Timeout = timeout;

 

                smtpClient.Credentials = CredentialCache.DefaultNetworkCredentials;

            }

        }

 

        protected override void ProcessRecord()

        {

            if ( attachments != null )

            {

                foreach ( FileInfo attachment in attachments )

                {

                    if ( ShouldProcess( attachment.FullName ) )

                    {

                        inputAttachments.Add( attachment );

                    }

                }

            }

        }

 

        protected override void EndProcessing()

        {

            try

            {

                if ( gmail.IsPresent )

                {

                    SendGmailMessage();

                }

                else

                {

                    SendSmtpMessage();

                }

            }

            finally

            {

                inputAttachments = null;

            }

        }

 

        private void SendGmailMessage()

        {

            IntPtr bstr = Marshal.SecureStringToBSTR( gmailPassword );

            string plainGmailPassword = Marshal.PtrToStringAuto( bstr );

            Marshal.ZeroFreeBSTR( bstr );

 

            string loginPostDataString = "&continue=" + HttpUtility.UrlEncode( GMAIL_ADDRESS ) +

                                        "&service=mail" +

                                        "&hl=en" +

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

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

 

            this.cookieCollection = new CookieCollection();

 

            this.WriteVerbose( "Sending Gmail login request..." );

 

            string loginResponse = MakeHttpWebRequest( GMAIL_LOGIN, true, false, Encoding.UTF8.GetBytes( loginPostDataString ) );

 

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

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

            {

                this.WriteError( new ErrorRecord(

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

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

            }

            else

            {

                this.multiPartBoundary = DateTime.Now.Ticks.ToString( "x" );

 

                string messageUrl = string.Format( "{0}{1}", GMAIL_ADDRESS, "?ui=1" );

 

                MemoryStream postDataStream = new MemoryStream();

                BinaryWriter postDataWriter = new BinaryWriter( postDataStream );

 

                Dictionary<string, string> variableHash = new Dictionary<string, string>();

 

                variableHash.Add( "view", "sm" );

                variableHash.Add( "at", this.cookieCollection[ "GMAIL_AT" ].Value );

                variableHash.Add( "to", string.Join( ", ", toArray ) );

                variableHash.Add( "cc", string.Join( ", ", ccArray ) );

                variableHash.Add( "bcc", string.Join( ", ", bccArray ) );

                variableHash.Add( "subject", subject );

                variableHash.Add( "ishtml", bodyIsHtml.IsPresent ? "1" : "0" );

                variableHash.Add( "msgbody", body );

 

                foreach ( string key in variableHash.Keys )

                {

                    postDataWriter.Write( Encoding.UTF8.GetBytes( string.Format(

                        "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n",

                        this.multiPartBoundary, key, variableHash[ key ] ) ) );

                }

 

                // add the attachments

                if ( inputAttachments.Count > 0 )

                {

                    messageUrl = string.Format( "{0}&newattach={1}", messageUrl, inputAttachments.Count );

 

                    byte[] attachmentData = new byte[ 0 ];

 

                    for ( int i = 0; i < inputAttachments.Count; i++ )

                    {

                        FileInfo attachment = inputAttachments[ i ];

                        FileStream fileStream = null;

 

                        try

                        {

                            fileStream = File.OpenRead( attachment.FullName );

                            byte[] fileData = new byte[ fileStream.Length ];

                            fileStream.Read( fileData, 0, fileData.Length );

 

                            postDataWriter.Write( Encoding.UTF8.GetBytes( string.Format(

                                "--{0}\r\nContent-Disposition: form-data; name=\"file{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n",

                                this.multiPartBoundary, i, attachment.Name, GetMimeType( attachment ) ) ) );

                            postDataWriter.Write( fileData );

                            postDataWriter.Write( Encoding.UTF8.GetBytes( "\r\n" ) );

                        }

                        catch ( Exception ex )

                        {

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

                        }

                        finally

                        {

                            if ( fileStream != null )

                            {

                                fileStream.Close();

                            }

                        }

                    }

                }

 

                postDataWriter.Write( Encoding.UTF8.GetBytes( string.Format( "--{0}--\r\n", this.multiPartBoundary ) ) );

                postDataWriter.Flush();

 

                byte[] messagePostData = postDataStream.ToArray();

 

                postDataStream.Close();

                postDataWriter.Close();

 

                this.WriteVerbose( "Sending Gmail message request..." );

 

                string messageResponse = MakeHttpWebRequest( messageUrl, true, true, messagePostData );

 

                // parse the message response

                messageResponse = Regex.Replace( messageResponse, "\n", "" );

                Match responseMatch = Regex.Match( messageResponse, @"D\(\[(?<Data>""sr"",[^)]+)\]\);" );

 

                if ( responseMatch.Success )

                {

                    string[] responseParts = Regex.Split( responseMatch.Groups[ "Data" ].Value, "," );

                    bool sent = ( responseParts[ 2 ] == "1" );

                    string message = Regex.Unescape( responseParts[ 3 ].Substring( 1, responseParts[ 3 ].Length - 2 ) );

 

                    if ( sent )

                    {

                        this.WriteObject( message );

                    }

                    else

                    {

                        this.WriteError( new ErrorRecord( new Exception( message ), "Send-Email", ErrorCategory.NotSpecified, this ) );

                    }

                }

            }

        }

 

        private string GetMimeType( FileInfo file )

        {

            string mimeType = "application/octet-stream";

 

            try

            {

                mimeType = Registry.ClassesRoot.OpenSubKey( file.Extension ).GetValue( "Content Type" ).ToString();

            }

            catch

            {

                // default to "application/octet-stream"

            }

 

            return mimeType;

        }

 

        private string MakeHttpWebRequest( string requestUrl, bool post, bool multiPart, 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.Timeout = timeout;

 

            webRequest.CookieContainer = new CookieContainer();

            webRequest.CookieContainer.Add( cookieCollection );

 

            if ( post )

            {

                webRequest.Method = "POST";

 

                if ( multiPart )

                {

                    webRequest.ContentType = string.Format( "multipart/form-data; boundary={0}", multiPartBoundary );

                }

                else

                {

                    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-Email", 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 or a <meta> refresh tag

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

                {

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

                }

                else

                {

                    Match urlMatch;

 

                    if ( ( urlMatch = Regex.Match( responseString, @"<meta\s*(http-equiv\s*=\s*""refresh"")?\s*content\s*=\s*""\s*0;\s*url\s*=\s*&#39;(?<URL>((?!&#39;).)*)&#39;" ) ).Success )

                    {

                        responseString = MakeHttpWebRequest( urlMatch.Groups[ "URL" ].Value.Replace( "&amp;", "&" ), false, false, null );

                    }

                }

            }

            catch ( Exception ex )

            {

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

            }

            finally

            {

                if ( webResponse != null )

                {

                    webResponse.Close();

                }

            }

 

            return responseString;

        }

 

        private void SendSmtpMessage()

        {

            try

            {

                MailMessage message = new MailMessage();

 

                message.From = new MailAddress( from );

 

                foreach ( string recipient in toArray )

                {

                    message.To.Add( recipient );

                }

 

                foreach ( string recipient in ccArray )

                {

                    message.CC.Add( recipient );

                }

 

                foreach ( string recipient in bccArray )

                {

                    message.Bcc.Add( recipient );

                }

 

                if ( !string.IsNullOrEmpty( subject ) )

                {

                    message.Subject = subject;

                }

 

                if ( !string.IsNullOrEmpty( body ) )

                {

                    message.Body = body;

                }

 

                message.IsBodyHtml = bodyIsHtml.IsPresent;

 

                foreach ( FileInfo attachment in inputAttachments )

                {

                    message.Attachments.Add( new Attachment( attachment.FullName ) );

                }

 

                if ( ShouldProcess( message.Subject ) )

                {

                    smtpClient.Send( message );

                }

            }

            catch ( Exception ex )

            {

                this.WriteError( new ErrorRecord( ex, "Send-Email", ErrorCategory.InvalidOperation, this ) );

            }

        }

    }

}


The Send-Email Cmdlet has one ParameterSet for Gmail messages and one for messages sent via SMTP. A Gmail SwitchParameter must be specified if a Gmail account is to be used. Just as the Post-Flickr authentication token can be stored in a variable in the user's profile, the Gmail username and password can also be stored in variables so they don't have to be typed in manually every time. The password must be stored in a SecureString. I wrote the following function to initialize these values:

function Init-Gmail
{
    [string]$global:GmailUsername = Read-Host "Gmail username"
    [System.Security.SecureString]$global:GmailPassword = Read-Host -AsSecureString "Gmail password"
}

Because they are marked as mandatory, if the username and password are not specified, PowerShell will ask for them:

PSH$ Send-Email -Gmail -To some.address@host.com -Subject "Email with PowerShell" -Body @"
>> I like PowerShell.
>>
>> Sincerely,
>> Jeff
>> "@
>>

cmdlet Send-Email at command pipeline position 1
Supply values for the following parameters:
GmailUsername: user.name
GmailPassword: *************
Your message has been sent.

And there you have it. This ends (for now) my series on the Cmdlets I have written. I realize that there really isn't a huge need for these Cmdlets out there, but that isn't really the point, is it? I wrote these to make my life easier and to have a little fun in the process. To me, that is the point of PowerShell.

Here is the XML help for Send-Email:

<?xml version="1.0" encoding="utf-8" ?>

<helpItems xmlns="http://msh" schema="maml">

    <command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">

        <command:details>

            <command:name>Send-Email</command:name>

            <maml:description>

                <maml:para>The Send-Email Cmdlet uses the System.Net.Mail.MailMessage class or a Gmail account to send an email message.</maml:para>

            </maml:description>

            <command:verb>Send</command:verb>

            <command:noun>Email</command:noun>

        </command:details>

        <maml:description>

            <maml:para>

              The Send-Email Cmdlet uses the System.Net.Mail.MailMessage class or a Gmail account to send an email message. Preferences for SMTP host name, port number, and from email address can be specified in your PowerShell profile by creating the following variables in the global scope:

 

              $EmailSmtpHost = "smtphost"

              $EmailSmtpPort = 527

              $EmailFrom = "name@email.com"

 

              When using the -Gmail switch parameter, it may be helpful to create these variables:

 

              $GmailUsername = "username"

              $Gmailpassword = &lt;SecureString password&gt;

            </maml:para>

        </maml:description>

        <command:syntax>

            <command:syntaxItem>

                <maml:name>Send-Email</maml:name>

                <command:parameter required="false">

                    <maml:name>gmail</maml:name>

                </command:parameter>

                <command:parameter required="true">

                    <maml:name>gmailUsername</maml:name>

                    <command:parameterValue required="true">string</command:parameterValue>

                </command:parameter>

                <command:parameter required="true">

                    <maml:name>gmailPassword</maml:name>

                    <command:parameterValue required="true">SecureString</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>smtpHost</maml:name>

                    <command:parameterValue required="true">string</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>portNumber</maml:name>

                    <command:parameterValue required="true">int</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>from</maml:name>

                    <command:parameterValue required="true">string</command:parameterValue>

                </command:parameter>

                <command:parameter required="true">

                    <maml:name>to</maml:name>

                    <command:parameterValue required="true">string []</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>cc</maml:name>

                    <command:parameterValue required="true">string []</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>bcc</maml:name>

                    <command:parameterValue required="true">string []</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>subject</maml:name>

                    <command:parameterValue required="true">string</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>body</maml:name>

                    <command:parameterValue required="true">string</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>bodyIsHtml</maml:name>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>attachment</maml:name>

                    <command:parameterValue required="true">FileInfo []</command:parameterValue>

                </command:parameter>

                <command:parameter required="false">

                    <maml:name>timeout</maml:name>

                    <command:parameterValue required="true">int</command:parameterValue>

                </command:parameter>

            </command:syntaxItem>

        </command:syntax>

        <command:parameters>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="false">

                <maml:name>Gmail</maml:name>

                <maml:description>

                    <maml:para>Indicates whether a Gmail account will be used to send the email message.</maml:para>

                </maml:description>

              <command:parameterValue required="true">SwitchParameter</command:parameterValue>

            </command:parameter>

            <command:parameter required="true" position="named" globbing="false" pipelineInput="false">

                <maml:name>GmailUsername</maml:name>

                <maml:description>

                    <maml:para>The Gmail username.</maml:para>

                </maml:description>

                <command:parameterValue required="true">string</command:parameterValue>

            </command:parameter>

            <command:parameter required="true" position="named" globbing="false" pipelineInput="false">

                <maml:name>GmailPassword</maml:name>

                <maml:description>

                    <maml:para>The Gmail password.</maml:para>

                </maml:description>

                <command:parameterValue required="true">SecureString</command:parameterValue>

            </command:parameter>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="false">

                <maml:name>SmtpHost</maml:name>

                <maml:description>

                    <maml:para>The SMTP host to use to send the email.</maml:para>

                </maml:description>

                <command:parameterValue required="true">string</command:parameterValue>

            </command:parameter>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="false">

                <maml:name>PortNumber</maml:name>

                <maml:description>

                    <maml:para>The port to use on the SMTP host.</maml:para>

                </maml:description>

                <command:parameterValue required="true">int</command:parameterValue>

                <dev:defaultValue>25</dev:defaultValue>

            </command:parameter>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="false">

                <maml:name>From</maml:name>

                <maml:description>

                    <maml:para>The sender of the email.</maml:para>

                </maml:description>

                <command:parameterValue required="true">string</command:parameterValue>

            </command:parameter>

            <command:parameter required="true" position="named" globbing="false" pipelineInput="false">

                <maml:name>To</maml:name>

                <maml:description>

                    <maml:para>The recipient(s) of the email.</maml:para>

                </maml:description>

                <command:parameterValue required="true">string []</command:parameterValue>

            </command:parameter>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="false">

                <maml:name>Cc</maml:name>

                <maml:description>

                    <maml:para>The carbon copy recipient(s) of the email.</maml:para>

                </maml:description>

                <command:parameterValue required="true">string []</command:parameterValue>

            </command:parameter>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="false">

                <maml:name>Bcc</maml:name>

                <maml:description>

                    <maml:para>The blind carbon copy recipient(s) of the email.</maml:para>

                </maml:description>

                <command:parameterValue required="true">string []</command:parameterValue>

            </command:parameter>

            <command:parameter required="true" position="named" globbing="false" pipelineInput="false">

                <maml:name>Subject</maml:name>

                <maml:description>

                    <maml:para>The subject of the email.</maml:para>

                </maml:description>

                <command:parameterValue required="true">string</command:parameterValue>

            </command:parameter>

            <command:parameter required="true" position="named" globbing="false" pipelineInput="false">

                <maml:name>Body</maml:name>

                <maml:description>

                    <maml:para>The body of the email.</maml:para>

                </maml:description>

                <command:parameterValue required="true">string</command:parameterValue>

            </command:parameter>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="false">

                <maml:name>BodyIsHtml</maml:name>

                <maml:description>

                    <maml:para>Indicates if the body of the email is HTML.</maml:para>

                </maml:description>

                <command:parameterValue required="true">SwitchParameter</command:parameterValue>

            </command:parameter>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="true">

                <maml:name>Attachment</maml:name>

                <maml:description>

                    <maml:para>The attachment(s) for the email.</maml:para>

                </maml:description>

                <command:parameterValue required="true">FileInfo []</command:parameterValue>

            </command:parameter>

            <command:parameter required="false" position="named" globbing="false" pipelineInput="false">

                <maml:name>Timeout</maml:name>

                <maml:description>

                    <maml:para>The timeout value for the SMTP server.</maml:para>

                </maml:description>

                <command:parameterValue required="true">int</command:parameterValue>

                <dev:defaultValue>60 seconds</dev:defaultValue>

            </command:parameter>

        </command:parameters>

        <command:inputTypes>

            <command:inputType>

                <dev:type>

                    <maml:name>FileInfo []</maml:name>

                    <maml:uri/>

                    <maml:description>

                        <maml:para>

                            Files to be added as attachments to the email.

                        </maml:para>

                    </maml:description>

                </dev:type>

        <maml:description>

        </maml:description>

            </command:inputType>

        </command:inputTypes>

    </command:command>

</helpItems>


If you end up building more than one of the Cmdlets I discussed in these posts, you will want to combine the help files into one.