diff --git a/assets/files.wxs b/assets/files.wxs
index 14c8a7c01a..4258c0df85 100644
--- a/assets/files.wxs
+++ b/assets/files.wxs
@@ -3042,6 +3042,15 @@
+
+
+
+
+
+
+
+
+
@@ -3874,6 +3883,9 @@
+
+
+
diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj
index 360e924773..f976d425b7 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj
+++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj
@@ -58,6 +58,7 @@
+
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Send-MailMessage.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Send-MailMessage.cs
index 5e91d0fdc4..0184a10395 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Send-MailMessage.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Send-MailMessage.cs
@@ -1,12 +1,12 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
-using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Management.Automation;
-using System.Net.Mail;
using System.Text;
+using MailKit;
+using MimeKit;
namespace Microsoft.PowerShell.Commands
{
@@ -14,7 +14,6 @@ namespace Microsoft.PowerShell.Commands
///
/// Implementation for the Send-MailMessage command.
///
- [Obsolete("This cmdlet does not guarantee secure connections to SMTP servers. While there is no immediate replacement available in PowerShell, we recommend you do not use Send-MailMessage at this time. See https://aka.ms/SendMailMessage for more information.")]
[Cmdlet(VerbsCommunications.Send, "MailMessage", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=135256")]
public sealed class SendMailMessage : PSCmdlet
{
@@ -28,7 +27,6 @@ public sealed class SendMailMessage : PSCmdlet
[Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("PsPath")]
- [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
public string[] Attachments { get; set; }
///
@@ -37,7 +35,6 @@ public sealed class SendMailMessage : PSCmdlet
///
[Parameter(ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
- [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
public string[] Bcc { get; set; }
///
@@ -71,8 +68,6 @@ public sealed class SendMailMessage : PSCmdlet
///
[Parameter(ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
- [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Cc")]
- [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
public string[] Cc { get; set; }
///
@@ -82,7 +77,7 @@ public sealed class SendMailMessage : PSCmdlet
[Parameter(ValueFromPipelineByPropertyName = true)]
[Alias("DNO")]
[ValidateNotNullOrEmpty]
- public DeliveryNotificationOptions DeliveryNotificationOption { get; set; }
+ public DeliveryStatusNotification DeliveryNotificationOption { get; set; }
///
/// Gets or sets the from address for this e-mail message. The default value for
@@ -107,7 +102,7 @@ public sealed class SendMailMessage : PSCmdlet
///
[Parameter(ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
- public MailPriority Priority { get; set; }
+ public MessagePriority Priority { get; set; } = MessagePriority.Normal;
///
/// Gets or sets the Reply-To field for this e-mail message.
@@ -127,7 +122,6 @@ public sealed class SendMailMessage : PSCmdlet
///
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
- [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
public string[] To { get; set; }
///
@@ -154,58 +148,76 @@ public sealed class SendMailMessage : PSCmdlet
[ValidateRange(0, Int32.MaxValue)]
public int Port { get; set; }
+ ///
+ /// Specifies the priority of the mail message.
+ ///
+ ///
+ /// Backward compability matching with System.Net.Mail.MailPriority Enum.
+ ///
+ public enum MessagePriority
+ {
+ ///
+ /// The email has low priority.
+ ///
+ Low,
+
+ ///
+ /// The email has normal priority.
+ ///
+ Normal,
+
+ ///
+ /// The email has high priority.
+ ///
+ High
+ }
+
#endregion
#region Private variables and methods
- // Instantiate a new instance of MailMessage
- private MailMessage _mMailMessage = new MailMessage();
+ private class DsnSmtpClient : MailKit.Net.Smtp.SmtpClient
+ {
+ public DeliveryStatusNotification DeliveryStatusNotification { get; set; }
+
+ protected override DeliveryStatusNotification? GetDeliveryStatusNotifications(MimeMessage message, MailboxAddress mailbox)
+ {
+ return DeliveryStatusNotification;
+ }
+ }
+
+ private DsnSmtpClient _smtpClient;
- private SmtpClient _mSmtpClient = null;
+ private PSVariable _globalEmailServer;
///
- /// Add the input addresses which are either string or hashtable to the MailMessage.
- /// It returns true if the from parameter has more than one value.
+ /// Add the input address to the MimeMessage list.
///
- ///
- ///
- private void AddAddressesToMailMessage(object address, string param)
+ /// MimeMessage InternetAddressList property to which single address is added.
+ /// String with unparsed mailbox addresses.
+ private void AddMailAddress(InternetAddressList list, string address)
{
- string[] objEmailAddresses = address as string[];
- foreach (string strEmailAddress in objEmailAddresses)
+ try
{
- try
- {
- switch (param)
- {
- case "to":
- {
- _mMailMessage.To.Add(new MailAddress(strEmailAddress));
- break;
- }
- case "cc":
- {
- _mMailMessage.CC.Add(new MailAddress(strEmailAddress));
- break;
- }
- case "bcc":
- {
- _mMailMessage.Bcc.Add(new MailAddress(strEmailAddress));
- break;
- }
- case "replyTo":
- {
- _mMailMessage.ReplyToList.Add(new MailAddress(strEmailAddress));
- break;
- }
- }
- }
- catch (FormatException e)
- {
- ErrorRecord er = new ErrorRecord(e, "FormatException", ErrorCategory.InvalidType, null);
- WriteError(er);
- continue;
- }
+ list.Add(MailboxAddress.Parse(new MimeKit.ParserOptions { AddressParserComplianceMode = RfcComplianceMode.Strict, AllowAddressesWithoutDomain = false }, address));
+ }
+ catch (MimeKit.ParseException ex)
+ {
+ ErrorRecord er = new ErrorRecord(ex, "FormatException", ErrorCategory.InvalidArgument, null); // Keep FormatException for error record for backward compability
+ WriteError(er);
+ }
+ }
+
+ ///
+ /// Add the input addresses to the MimeMessage list.
+ ///
+ /// MimeMessage InternetAddressList property to which addresses are added.
+ /// String array with unparsed mailbox addresses.
+ private void AddMailAddresses(InternetAddressList list, string[] addresses)
+ {
+ foreach (var strEmailAddress in addresses)
+ {
+ AddMailAddress(list, strEmailAddress);
}
}
@@ -218,165 +230,203 @@ private void AddAddressesToMailMessage(object address, string param)
///
protected override void BeginProcessing()
{
- try
- {
- // Set the sender address of the mail message
- _mMailMessage.From = new MailAddress(From);
- }
- catch (FormatException e)
- {
- ErrorRecord er = new ErrorRecord(e, "FormatException", ErrorCategory.InvalidType, From);
- ThrowTerminatingError(er);
- }
+ _smtpClient = new DsnSmtpClient();
- // Set the recipient address of the mail message
- AddAddressesToMailMessage(To, "to");
+ // Get the PowerShell environment variable
+ // PSEmailServer might be null if it is deleted by: PS> del variable:PSEmailServer
+ _globalEmailServer = SessionState.Internal.GetVariable(SpecialVariables.PSEmailServer);
+ }
- // Set the BCC address of the mail message
- if (Bcc != null)
- {
- AddAddressesToMailMessage(Bcc, "bcc");
- }
+ ///
+ /// ProcessRecord override.
+ ///
+ protected override void ProcessRecord()
+ {
+ // Fallback to global email server if SmtpServer parameter is not set
+ SmtpServer = SmtpServer ?? Convert.ToString(_globalEmailServer?.Value, CultureInfo.InvariantCulture);
- // Set the CC address of the mail message
- if (Cc != null)
+ if (string.IsNullOrEmpty(SmtpServer))
{
- AddAddressesToMailMessage(Cc, "cc");
+ ErrorRecord er = new ErrorRecord(new InvalidOperationException(SendMailMessageStrings.HostNameValue), null, ErrorCategory.InvalidArgument, null);
+ ThrowTerminatingError(er);
}
- // Set the Reply-To address of the mail message
- if (ReplyTo != null)
+ // Set default port for protocol
+ if (Port == 0)
{
- AddAddressesToMailMessage(ReplyTo, "replyTo");
+ if (UseSsl)
+ {
+ Port = 465; // Standard SMTPS port
+ }
+ else
+ {
+ Port = 25; // Standard SMTP port
+ }
}
- // Set the delivery notification
- _mMailMessage.DeliveryNotificationOptions = DeliveryNotificationOption;
-
- // Set the subject of the mail message
- _mMailMessage.Subject = Subject;
-
- // Set the body of the mail message
- _mMailMessage.Body = Body;
-
- // Set the subject and body encoding
- _mMailMessage.SubjectEncoding = Encoding;
- _mMailMessage.BodyEncoding = Encoding;
-
- // Set the format of the mail message body as HTML
- _mMailMessage.IsBodyHtml = BodyAsHtml;
+ // Create mail message
+ var msg = new MimeMessage();
- // Set the priority of the mail message to normal
- _mMailMessage.Priority = Priority;
+ // Set the sender address of the mail message
+ AddMailAddress(msg.From, From);
- // Get the PowerShell environment variable
- // globalEmailServer might be null if it is deleted by: PS> del variable:PSEmailServer
- PSVariable globalEmailServer = SessionState.Internal.GetVariable(SpecialVariables.PSEmailServer);
+ // Set the recipient addresses of the mail message
+ AddMailAddresses(msg.To, To);
- if (SmtpServer == null && globalEmailServer != null)
+ // Set the CC addresses of the mail message
+ if (Cc != null)
{
- SmtpServer = Convert.ToString(globalEmailServer.Value, CultureInfo.InvariantCulture);
+ AddMailAddresses(msg.Cc, Cc);
}
- if (string.IsNullOrEmpty(SmtpServer))
+ // Set the BCC addresses of the mail message
+ if (Bcc != null)
{
- ErrorRecord er = new ErrorRecord(new InvalidOperationException(SendMailMessageStrings.HostNameValue), null, ErrorCategory.InvalidArgument, null);
- this.ThrowTerminatingError(er);
+ AddMailAddresses(msg.Bcc, Bcc);
}
- if (Port == 0)
- {
- _mSmtpClient = new SmtpClient(SmtpServer);
- }
- else
+ // Set the Reply-To addresses of the mail message
+ if (ReplyTo != null)
{
- _mSmtpClient = new SmtpClient(SmtpServer, Port);
+ AddMailAddresses(msg.ReplyTo, ReplyTo);
}
- if (UseSsl)
+ // Set the subject of the mail message
+ if (Subject != null)
{
- _mSmtpClient.EnableSsl = true;
+ msg.Subject = Subject;
}
- if (Credential != null)
+ // Set the priority of the mail message
+ msg.Priority = (MimeKit.MessagePriority)Priority;
+
+ // Create body
+ var builder = new BodyBuilder();
+
+ if (BodyAsHtml)
{
- _mSmtpClient.UseDefaultCredentials = false;
- _mSmtpClient.Credentials = Credential.GetNetworkCredential();
+ builder.HtmlBody = Body;
}
- else if (!UseSsl)
+ else
{
- _mSmtpClient.UseDefaultCredentials = true;
+ builder.TextBody = Body;
}
- }
- ///
- /// ProcessRecord override.
- ///
- protected override void ProcessRecord()
- {
// Add the attachments
- if (Attachments != null)
+ /* Note for second check:
+ * The solution below is a workaround to check if the Attachments parameter is null for the PSCustomObject piped into the cmdlet
+ * and therefore the Attachments parameter is falsely set to the casted PSCustomObject.
+ *
+ * Attachments parameter is not mandatory but declared as ValueFromPipeline and ValueFromPipelineByPropertyName.
+ * If PSCustomObject is piped into cmdlet and Attachments property is not present (ValueFromPipelineByPropertyName),
+ * than the binding process will try to set Attachments to the piped PSCustomObject (ValueFromPipeline).
+ *
+ * Problem: PSCustomObject (Pipeline input) can be casted to string[] (Attachments parameter)
+ * Attachments will hold at least one string with the current pipeline object as a string. E.g. "@{SmtpServer=localhost, From=foo@contonso.com, To=bar@contonso.com}"
+ *
+ * A simple check if Attachments starts with "@{" or even a pattern match might lead to problems, because the file name for the
+ * attachment could THEORETICALLY be "@{SmtpServer=localhost, From=foo@contonso.com, To=bar@contonso.com}" without any extension.
+ *
+ * The problem only occurs for parameters which are not mandatory but have [ValueFromPipeline] and [ValueFromPipelineByPropertyName] attribute.
+ */
+ if (Attachments != null && Attachments?[0] != CurrentPipelineObject.ToString())
{
- string filepath = string.Empty;
foreach (string attachFile in Attachments)
{
try
{
- filepath = PathUtils.ResolveFilePath(attachFile, this);
+ builder.Attachments.Add(attachFile);
}
- catch (ItemNotFoundException e)
+ catch (ArgumentException ex)
{
- // NOTE: This will throw
- PathUtils.ReportFileOpenFailure(this, filepath, e);
+ ErrorRecord er = new ErrorRecord(ex, "ArgumentException", ErrorCategory.InvalidArgument, builder);
+ WriteError(er);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ ErrorRecord er = new ErrorRecord(ex, "UnauthorizedAccessException", ErrorCategory.PermissionDenied, builder);
+ WriteError(er);
+ }
+ catch (System.IO.DirectoryNotFoundException ex)
+ {
+ ErrorRecord er = new ErrorRecord(ex, "DirectoryNotFoundException", ErrorCategory.InvalidArgument, builder);
+ WriteError(er);
+ }
+ catch (System.IO.FileNotFoundException ex)
+ {
+ ErrorRecord er = new ErrorRecord(ex, "FileNotFoundException", ErrorCategory.ObjectNotFound, builder);
+ WriteError(er);
+ }
+ catch (System.IO.IOException ex)
+ {
+ ErrorRecord er = new ErrorRecord(ex, "IOException", ErrorCategory.ReadError, builder);
+ WriteError(er);
}
-
- Attachment mailAttachment = new Attachment(filepath);
- _mMailMessage.Attachments.Add(mailAttachment);
}
}
- }
- ///
- /// EndProcessing override.
- ///
- protected override void EndProcessing()
- {
+ // Set the body of the mail message
+ msg.Body = builder.ToMessageBody();
+
try
{
+ // Connect to SMTP server
+ _smtpClient.Connect(SmtpServer, Port, UseSsl);
+
+ // Authenticate if credentials are provided
+ if (Credential != null)
+ {
+ _smtpClient.Authenticate(Credential.GetNetworkCredential());
+ }
+
+ // Set the delivery notification
+ _smtpClient.DeliveryStatusNotification = DeliveryNotificationOption;
+
// Send the mail message
- _mSmtpClient.Send(_mMailMessage);
+ _smtpClient.Send(msg);
}
- catch (SmtpFailedRecipientsException ex)
+ catch (MailKit.ProtocolException ex)
{
- ErrorRecord er = new ErrorRecord(ex, "SmtpFailedRecipientsException", ErrorCategory.InvalidOperation, _mSmtpClient);
+ ErrorRecord er = new ErrorRecord(ex, "ProtocolException", ErrorCategory.ProtocolError, _smtpClient);
WriteError(er);
}
- catch (SmtpException ex)
+ catch (MailKit.Security.AuthenticationException ex)
{
- if (ex.InnerException != null)
- {
- ErrorRecord er = new ErrorRecord(new SmtpException(ex.InnerException.Message), "SmtpException", ErrorCategory.InvalidOperation, _mSmtpClient);
- WriteError(er);
- }
- else
- {
- ErrorRecord er = new ErrorRecord(ex, "SmtpException", ErrorCategory.InvalidOperation, _mSmtpClient);
- WriteError(er);
- }
+ ErrorRecord er = new ErrorRecord(ex, "AuthenticationException", ErrorCategory.AuthenticationError, _smtpClient);
+ WriteError(er);
}
catch (InvalidOperationException ex)
{
- ErrorRecord er = new ErrorRecord(ex, "InvalidOperationException", ErrorCategory.InvalidOperation, _mSmtpClient);
+ ErrorRecord er = new ErrorRecord(ex, "InvalidOperationException", ErrorCategory.InvalidOperation, _smtpClient);
+ WriteError(er);
+ }
+ catch (System.IO.IOException ex)
+ {
+ ErrorRecord er = new ErrorRecord(ex, "IOException", ErrorCategory.ReadError, builder);
WriteError(er);
}
- catch (System.Security.Authentication.AuthenticationException ex)
+ catch (System.Net.Sockets.SocketException ex)
{
- ErrorRecord er = new ErrorRecord(ex, "AuthenticationException", ErrorCategory.InvalidOperation, _mSmtpClient);
+ ErrorRecord er = new ErrorRecord(ex, "SocketException", ErrorCategory.ConnectionError, builder);
WriteError(er);
}
+ catch (ArgumentNullException ex)
+ {
+ ErrorRecord er = new ErrorRecord(ex, "ArgumentNullException", ErrorCategory.InvalidArgument, _smtpClient);
+ WriteError(er);
+ }
+ finally
+ {
+ _smtpClient.Disconnect(true);
+ }
+ }
- // If we don't dispose the attachments, the sender can't modify or use the files sent.
- _mMailMessage.Attachments.Dispose();
+ ///
+ /// EndProcessing override.
+ ///
+ protected override void EndProcessing()
+ {
+ _smtpClient?.Dispose();
}
#endregion
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Send-MailMessage.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Send-MailMessage.Tests.ps1
index cb05eabb8b..c0e66fac5b 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Send-MailMessage.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Send-MailMessage.Tests.ps1
@@ -109,21 +109,6 @@ Describe "Send-MailMessage DRT Unit Tests" -Tags CI, RequireSudoOnUnix {
}
)
- It "Shows obsolete message for cmdlet" {
- $server | Should -Not -Be $null
-
- $powershell = [PowerShell]::Create()
-
- $null = $powershell.AddCommand("Send-MailMessage").AddParameters($testCases[0].InputObject).AddParameter("ErrorAction","SilentlyContinue")
-
- $powershell.Invoke()
-
- $warnings = $powershell.Streams.Warning
-
- $warnings.count | Should -BeGreaterThan 0
- $warnings[0].ToString() | Should -BeLike "The command 'Send-MailMessage' is obsolete. *"
- }
-
It "Can send mail message using named parameters " -TestCases $testCases {
param($InputObject)
@@ -156,11 +141,9 @@ Describe "Send-MailMessage DRT Unit Tests" -Tags CI, RequireSudoOnUnix {
$mail.MessageParts[0].BodyData | Should -BeExactly $InputObject.Body
}
- It "Can send mail message using pipline named parameters " -TestCases $testCases -Pending {
+ It "Can send mail message using pipline named parameters " -TestCases $testCases {
param($InputObject)
- Set-TestInconclusive "As of right now the Send-MailMessage cmdlet does not support piping named parameters (see issue 7591)"
-
$server | Should -Not -Be $null
[PsCustomObject]$InputObject | Send-MailMessage -ErrorAction SilentlyContinue
@@ -287,8 +270,8 @@ Describe "Send-MailMessage Feature Tests" -Tags Feature, RequireSudoOnUnix {
}
It "Can send mail with attachments" {
- $attachment1 = "TestDrive:\attachment1.txt"
- $attachment2 = "TestDrive:\attachment2.txt"
+ $attachment1 = "$TestDrive/attachment1.txt"
+ $attachment2 = "$TestDrive/attachment2.png"
$pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAnElEQVR42u3RAQ0AAAgDoL9/aK3hHFSgyUw4o0KEIEQIQoQgRAhChAgRghAhCBGCECEIEYIQhAhBiBCECEGIEIQgRAhChCBECEKEIAQhQhAiBCFCECIEIQgRghAhCBGCECEIQYgQhAhBiBCECEEIQoQgRAhChCBECEIQIgQhQhAiBCFCEIIQIQgRghAhCBGCECFChCBECEKEIOS7BU5Hx50BmcQaAAAAAElFTkSuQmCC"
@@ -302,9 +285,7 @@ Describe "Send-MailMessage Feature Tests" -Tags Feature, RequireSudoOnUnix {
$mail = Read-Mail
$mail.MessageParts.Count | Should -BeExactly 3
- $txt = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($mail.MessageParts[1].BodyData)) -replace "`n|`r"
- $txt | Should -BeExactly "First attachment"
-
+ ($mail.MessageParts[1].BodyData) | Should -BeExactly "First attachment"
($mail.MessageParts[2].BodyData -replace "`n|`r") | Should -BeExactly $pngBase64
}
}