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*'(?<URL>((?!').)*)'" ) ).Success )
{
responseString = MakeHttpWebRequest( urlMatch.Groups[ "URL" ].Value.Replace( "&", "&" ), 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:
{
[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:
>> 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 = <SecureString password>
</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.
No comments:
Post a Comment