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 } }