1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
2 
3 using System.Collections.Generic;
4 using System.Diagnostics.CodeAnalysis;
5 using System.IO;
6 using System.Net;
7 using System.Net.Mail;
8 using System.Text;
9 using System.Web.Helpers.Resources;
10 using System.Web.WebPages.Scope;
11 using Microsoft.Internal.Web.Utils;
12 
13 namespace System.Web.Helpers
14 {
15     [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "WebMail", Justification = "The name of this class is consistent with the naming convention followed in other helpers")]
16     public static class WebMail
17     {
18         internal static readonly object SmtpServerKey = new object();
19         internal static readonly object SmtpPortKey = new object();
20         internal static readonly object SmtpUseDefaultCredentialsKey = new object();
21         internal static readonly object EnableSslKey = new object();
22         internal static readonly object PasswordKey = new object();
23         internal static readonly object UserNameKey = new object();
24         internal static readonly object FromKey = new object();
25         internal static readonly Lazy<IDictionary<object, object>> SmtpDefaults = new Lazy<IDictionary<object, object>>(ReadSmtpDefaults);
26 
27         /// <summary>
28         /// MailMessage dictates that headers values that have equivalent properties would be discarded or overwritten. The list of values is available at
29         /// http://msdn.microsoft.com/en-us/library/system.net.mail.mailmessage.aspx
30         /// </summary>
31         private static readonly Dictionary<string, Action<MailMessage, string>> _actionableHeaders = new Dictionary<string, Action<MailMessage, string>>(StringComparer.OrdinalIgnoreCase)
32         {
33             { "Bcc", (message, value) => message.Bcc.Add(value) },
34             { "Cc", (message, value) => message.CC.Add(value) },
35             { "From", (mailMessage, value) =>
36             {
37                 mailMessage.From = new MailAddress(value);
38             }
39                 },
40             { "Priority", SetPriority },
41             { "Reply-To", (mailMessage, value) =>
42             {
43                 mailMessage.ReplyToList.Add(value);
44             }
45                 },
46             { "Sender", (mailMessage, value) =>
47             {
48                 mailMessage.Sender = new MailAddress(value);
49             }
50                 },
51             { "To", (mailMessage, value) =>
52             {
53                 mailMessage.To.Add(value);
54             }
55                 },
56         };
57 
58         ///////////////////////////////////////////////////////////////////////////
59         // Public Properties
60         [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "SmtpServer is more descriptive as compared to the actual argument \"value\"")]
61         public static string SmtpServer
62         {
63             get { return ReadValue<string>(SmtpServerKey); }
64             set
65             {
66                 if (String.IsNullOrEmpty(value))
67                 {
68                     throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "SmtpServer");
69                 }
70                 ScopeStorage.CurrentScope[SmtpServerKey] = value;
71             }
72         }
73 
74         public static int SmtpPort
75         {
76             get { return ReadValue<int>(SmtpPortKey); }
77             set { ScopeStorage.CurrentScope[SmtpPortKey] = value; }
78         }
79 
80         public static string From
81         {
82             get { return ReadValue<string>(FromKey); }
83             set { ScopeStorage.CurrentScope[FromKey] = value; }
84         }
85 
86         public static bool SmtpUseDefaultCredentials
87         {
88             get { return ReadValue<bool>(SmtpUseDefaultCredentialsKey); }
89             set { ScopeStorage.CurrentScope[SmtpUseDefaultCredentialsKey] = value; }
90         }
91 
92         public static bool EnableSsl
93         {
94             get { return ReadValue<bool>(EnableSslKey); }
95             set { ScopeStorage.CurrentScope[EnableSslKey] = value; }
96         }
97 
98         public static string UserName
99         {
100             get { return ReadValue<string>(UserNameKey); }
101             set { ScopeStorage.CurrentScope[UserNameKey] = value; }
102         }
103 
104         public static string Password
105         {
106             get { return ReadValue<string>(PasswordKey); }
107             set { ScopeStorage.CurrentScope[PasswordKey] = value; }
108         }
109 
Send(string to, string subject, string body, string from = null, string cc = null, IEnumerable<string> filesToAttach = null, bool isBodyHtml = true, IEnumerable<string> additionalHeaders = null, string bcc = null, string contentEncoding = null, string headerEncoding = null, string priority = null, string replyTo = null)110         public static void Send(string to,
111                                 string subject,
112                                 string body,
113                                 string from = null,
114                                 string cc = null,
115                                 IEnumerable<string> filesToAttach = null,
116                                 bool isBodyHtml = true,
117                                 IEnumerable<string> additionalHeaders = null,
118                                 string bcc = null,
119                                 string contentEncoding = null,
120                                 string headerEncoding = null,
121                                 string priority = null,
122                                 string replyTo = null)
123         {
124             if (filesToAttach != null)
125             {
126                 foreach (string fileName in filesToAttach)
127                 {
128                     if (String.IsNullOrEmpty(fileName))
129                     {
130                         throw new ArgumentException(HelpersResources.WebMail_ItemInCollectionIsNull, "filesToAttach");
131                     }
132                 }
133             }
134 
135             if (additionalHeaders != null)
136             {
137                 foreach (string header in additionalHeaders)
138                 {
139                     if (String.IsNullOrEmpty(header))
140                     {
141                         throw new ArgumentException(HelpersResources.WebMail_ItemInCollectionIsNull, "additionalHeaders");
142                     }
143                 }
144             }
145 
146             MailPriority priorityValue = MailPriority.Normal;
147             if (!String.IsNullOrEmpty(priority) && !ConversionUtil.TryFromStringToEnum(priority, out priorityValue))
148             {
149                 throw new ArgumentException(HelpersResources.WebMail_InvalidPriority, "priority");
150             }
151 
152             if (String.IsNullOrEmpty(SmtpServer))
153             {
154                 throw new InvalidOperationException(HelpersResources.WebMail_SmtpServerNotSpecified);
155             }
156 
157             using (MailMessage message = new MailMessage())
158             {
159                 SetPropertiesOnMessage(message, to, subject, body, from, cc, bcc, replyTo, contentEncoding, headerEncoding, priorityValue,
160                                        filesToAttach, isBodyHtml, additionalHeaders);
161                 using (SmtpClient client = new SmtpClient())
162                 {
163                     SetPropertiesOnClient(client);
164                     client.Send(message);
165                 }
166             }
167         }
168 
ReadValue(object key)169         private static TValue ReadValue<TValue>(object key)
170         {
171             return (TValue)(ScopeStorage.CurrentScope[key] ?? SmtpDefaults.Value[key]);
172         }
173 
ReadSmtpDefaults()174         private static IDictionary<object, object> ReadSmtpDefaults()
175         {
176             Dictionary<object, object> smtpDefaults = new Dictionary<object, object>();
177             try
178             {
179                 // Create a new SmtpClient object: this will read config & tell us what the default value is
180                 using (SmtpClient client = new SmtpClient())
181                 {
182                     smtpDefaults[SmtpServerKey] = client.Host;
183                     smtpDefaults[SmtpPortKey] = client.Port;
184                     smtpDefaults[EnableSslKey] = client.EnableSsl;
185                     smtpDefaults[SmtpUseDefaultCredentialsKey] = client.UseDefaultCredentials;
186 
187                     var credentials = client.Credentials as NetworkCredential;
188                     if (credentials != null)
189                     {
190                         smtpDefaults[UserNameKey] = credentials.UserName;
191                         smtpDefaults[PasswordKey] = credentials.Password;
192                     }
193                     else
194                     {
195                         smtpDefaults[UserNameKey] = null;
196                         smtpDefaults[PasswordKey] = null;
197                     }
198                     using (MailMessage message = new MailMessage())
199                     {
200                         smtpDefaults[FromKey] = (message.From != null) ? message.From.Address : null;
201                     }
202                 }
203             }
204             catch (InvalidOperationException)
205             {
206                 // Due to Bug Dev10 PS 337470 ("SmtpClient reports InvalidOperationException when disposed"), we need to ignore the spurious InvalidOperationException
207             }
208             return smtpDefaults;
209         }
210 
SetPropertiesOnClient(SmtpClient client)211         internal static void SetPropertiesOnClient(SmtpClient client)
212         {
213             // If no value has been assigned to these properties, at the very worst we will simply
214             // write back the values we just read from the SmtpClient
215             if (SmtpServer != null)
216             {
217                 client.Host = SmtpServer;
218             }
219             client.Port = SmtpPort;
220             client.UseDefaultCredentials = SmtpUseDefaultCredentials;
221             client.EnableSsl = EnableSsl;
222             if (!String.IsNullOrEmpty(UserName))
223             {
224                 client.Credentials = new NetworkCredential(UserName, Password);
225             }
226         }
227 
SetPropertiesOnMessage(MailMessage message, string to, string subject, string body, string from, string cc, string bcc, string replyTo, string contentEncoding, string headerEncoding, MailPriority priority, IEnumerable<string> filesToAttach, bool isBodyHtml, IEnumerable<string> additionalHeaders)228         internal static void SetPropertiesOnMessage(MailMessage message, string to, string subject,
229                                                     string body, string from, string cc, string bcc, string replyTo,
230                                                     string contentEncoding, string headerEncoding, MailPriority priority,
231                                                     IEnumerable<string> filesToAttach, bool isBodyHtml,
232                                                     IEnumerable<string> additionalHeaders)
233         {
234             message.Subject = subject;
235             message.Body = body;
236             message.IsBodyHtml = isBodyHtml;
237 
238             if (additionalHeaders != null)
239             {
240                 AssignHeaderValues(message, additionalHeaders);
241             }
242 
243             if (to != null)
244             {
245                 message.To.Add(to);
246             }
247 
248             if (!String.IsNullOrEmpty(cc))
249             {
250                 message.CC.Add(cc);
251             }
252 
253             if (!String.IsNullOrEmpty(bcc))
254             {
255                 message.Bcc.Add(bcc);
256             }
257 
258             if (!String.IsNullOrEmpty(replyTo))
259             {
260                 message.ReplyToList.Add(replyTo);
261             }
262 
263             if (!String.IsNullOrEmpty(contentEncoding))
264             {
265                 message.BodyEncoding = Encoding.GetEncoding(contentEncoding);
266             }
267 
268             if (!String.IsNullOrEmpty(headerEncoding))
269             {
270                 message.HeadersEncoding = Encoding.GetEncoding(headerEncoding);
271             }
272 
273             message.Priority = priority;
274 
275             if (from != null)
276             {
277                 message.From = new MailAddress(from);
278             }
279             else if (!String.IsNullOrEmpty(From))
280             {
281                 message.From = new MailAddress(From);
282             }
283             else if (message.From == null || String.IsNullOrEmpty(message.From.Address))
284             {
285                 var httpContext = HttpContext.Current;
286                 if (httpContext != null)
287                 {
288                     message.From = new MailAddress("DoNotReply@" + httpContext.Request.Url.Host);
289                 }
290                 else
291                 {
292                     throw new InvalidOperationException(HelpersResources.WebMail_UnableToDetermineFrom);
293                 }
294             }
295 
296             if (filesToAttach != null)
297             {
298                 foreach (string file in filesToAttach)
299                 {
300                     if (!Path.IsPathRooted(file) && HttpRuntime.AppDomainAppPath != null)
301                     {
302                         message.Attachments.Add(new Attachment(Path.Combine(HttpRuntime.AppDomainAppPath, file)));
303                     }
304                     else
305                     {
306                         message.Attachments.Add(new Attachment(file));
307                     }
308                 }
309             }
310         }
311 
AssignHeaderValues(MailMessage message, IEnumerable<string> headerValues)312         internal static void AssignHeaderValues(MailMessage message, IEnumerable<string> headerValues)
313         {
314             // Parse the header value. If this
315             foreach (var header in headerValues)
316             {
317                 string key, value;
318                 if (TryParseHeader(header, out key, out value))
319                 {
320                     // Verify if the header key maps to a property on MailMessage.
321                     Action<MailMessage, string> action;
322                     if (_actionableHeaders.TryGetValue(key, out action))
323                     {
324                         try
325                         {
326                             action(message, value);
327                         }
328                         catch (FormatException)
329                         {
330                             // If the mail address is invalid, swallow the exception.
331                         }
332                     }
333                     message.Headers.Add(key, value);
334                 }
335             }
336         }
337 
338         /// <summary>
339         /// Parses a SMTP Mail header of the format "name: value"
340         /// </summary>
341         /// <returns>True if the header was parsed.</returns>
TryParseHeader(string header, out string key, out string value)342         internal static bool TryParseHeader(string header, out string key, out string value)
343         {
344             int pos = header.IndexOf(':');
345             if (pos > 0)
346             {
347                 key = header.Substring(0, pos).TrimEnd();
348                 value = header.Substring(pos + 1).TrimStart();
349                 return key.Length > 0 && value.Length > 0;
350             }
351             key = null;
352             value = null;
353             return false;
354         }
355 
SetPriority(MailMessage message, string priority)356         private static void SetPriority(MailMessage message, string priority)
357         {
358             MailPriority priorityValue;
359             if (!String.IsNullOrEmpty(priority) && ConversionUtil.TryFromStringToEnum(priority, out priorityValue))
360             {
361                 // If we can parse it, set it. Do nothing otherwise
362                 message.Priority = priorityValue;
363             }
364         }
365     }
366 }
367