You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

239 lines
11 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using MsgReader.Mime.Decode;
namespace MsgReader.Mime.Header
{
/// <summary>
/// This class is used for RFC compliant email addresses.<br/>
/// <br/>
/// The class cannot be instantiated from outside the library.
/// </summary>
/// <remarks>
/// The <seealso cref="MailAddress"/> does not cover all the possible formats
/// for <a href="http://tools.ietf.org/html/rfc5322#section-3.4">RFC 5322 section 3.4</a> compliant email addresses.
/// This class is used as an address wrapper to account for that deficiency.
/// </remarks>
public class RfcMailAddress
{
#region Properties
///<summary>
/// The email address of this <see cref="RfcMailAddress"/><br/>
/// It is possibly string.Empty since RFC mail addresses does not require an email address specified.
///</summary>
///<example>
/// Example header with email address:<br/>
/// To: <c>Test test@mail.com</c><br/>
/// Address will be <c>test@mail.com</c><br/>
///</example>
///<example>
/// Example header without email address:<br/>
/// To: <c>Test</c><br/>
/// Address will be <see cref="string.Empty"/>.
///</example>
public string Address { get; }
///<summary>
/// The display name of this <see cref="RfcMailAddress"/><br/>
/// It is possibly <see cref="string.Empty"/> since RFC mail addresses does not require a display name to be specified.
///</summary>
///<example>
/// Example header with display name:<br/>
/// To: <c>Test test@mail.com</c><br/>
/// DisplayName will be <c>Test</c>
///</example>
///<example>
/// Example header without display name:<br/>
/// To: <c>test@test.com</c><br/>
/// DisplayName will be <see cref="string.Empty"/>
///</example>
public string DisplayName { get; }
/// <summary>
/// This is the Raw string used to describe the <see cref="RfcMailAddress"/>.
/// </summary>
public string Raw { get; }
/// <summary>
/// The <see cref="MailAddress"/> associated with the <see cref="RfcMailAddress"/>.
/// </summary>
/// <remarks>
/// The value of this property can be <see lanword="null"/> in instances where the <see cref="MailAddress"/> cannot represent the address properly.<br/>
/// Use <see cref="HasValidMailAddress"/> property to see if this property is valid.
/// </remarks>
public MailAddress MailAddress { get; }
/// <summary>
/// Specifies if the object contains a valid <see cref="MailAddress"/> reference.
/// </summary>
public bool HasValidMailAddress => MailAddress != null;
#endregion
#region Constructors
/// <summary>
/// Constructs an <see cref="RfcMailAddress"/> object from a <see cref="MailAddress"/> object.<br/>
/// This constructor is used when we were able to construct a <see cref="MailAddress"/> from a string.
/// </summary>
/// <param name="mailAddress">The address that <paramref name="raw"/> was parsed into</param>
/// <param name="raw">The raw unparsed input which was parsed into the <paramref name="mailAddress"/></param>
/// <exception cref="ArgumentNullException">If <paramref name="mailAddress"/> or <paramref name="raw"/> is <see langword="null"/></exception>
private RfcMailAddress(MailAddress mailAddress, string raw)
{
if (mailAddress == null)
throw new ArgumentNullException(nameof(mailAddress));
if (raw == null)
throw new ArgumentNullException(nameof(raw));
MailAddress = mailAddress;
Address = mailAddress.Address;
DisplayName = mailAddress.DisplayName;
Raw = raw;
}
/// <summary>
/// When we were unable to parse a string into a <see cref="MailAddress"/>, this constructor can be
/// used. The Raw string is then used as the <see cref="DisplayName"/>.
/// </summary>
/// <param name="raw">The raw unparsed input which could not be parsed</param>
/// <exception cref="ArgumentNullException">If <paramref name="raw"/> is <see langword="null"/></exception>
private RfcMailAddress(string raw)
{
if (raw == null)
throw new ArgumentNullException(nameof(raw));
MailAddress = null;
Address = string.Empty;
DisplayName = raw;
Raw = raw;
}
#endregion
#region ToString
/// <summary>
/// A string representation of the <see cref="RfcMailAddress"/> object
/// </summary>
/// <returns>Returns the string representation for the object</returns>
public override string ToString()
{
return HasValidMailAddress ? MailAddress.ToString() : Raw;
}
#endregion
#region Parsing
/// <summary>
/// Parses an email address from a MIME header<br/>
/// <br/>
/// Examples of input:
/// <c>Eksperten mailrobot &lt;noreply@mail.eksperten.dk&gt;</c><br/>
/// <c>"Eksperten mailrobot" &lt;noreply@mail.eksperten.dk&gt;</c><br/>
/// <c>&lt;noreply@mail.eksperten.dk&gt;</c><br/>
/// <c>noreply@mail.eksperten.dk</c><br/>
/// <br/>
/// It might also contain encoded text, which will then be decoded.
/// </summary>
/// <param name="input">The value to parse out and email and/or a username</param>
/// <returns>A <see cref="RfcMailAddress"/></returns>
/// <exception cref="ArgumentNullException">If <paramref name="input"/> is <see langword="null"/></exception>
/// <remarks>
/// <see href="http://tools.ietf.org/html/rfc5322#section-3.4">RFC 5322 section 3.4</see> for more details on email syntax.<br/>
/// <see cref="EncodedWord.Decode">For more information about encoded text</see>.
/// </remarks>
internal static RfcMailAddress ParseMailAddress(string input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
// Decode the value, if it was encoded
input = EncodedWord.Decode(input.Trim());
//Remove any redundant sets of angle brackets around the email address
var lastOpenAngleBracketIdx = input.LastIndexOf('<');
var lastCloseAngleBracketIdx = input.LastIndexOf('>');
//Find the index of the first angle bracket in this series of angle brackets, e.g "a>b" <<blah@email.com>> wouldn't find the angle bracket in the display name
var firstOpenAngleBracketIdx = lastOpenAngleBracketIdx;
var firstCloseAngleBracketIdx = lastCloseAngleBracketIdx;
while (firstOpenAngleBracketIdx > 0 && //There is a character before the last open angle bracket
input[firstOpenAngleBracketIdx - 1] == '<' && //The character before the last open angle bracket is another open angle bracket
input[firstCloseAngleBracketIdx - 1] == '>') //The character before the last close angle bracket is another close angle bracket
{
//Update the first angle bracket indices
firstOpenAngleBracketIdx--;
firstCloseAngleBracketIdx--;
}
//If the email address in the input string is enclosed in multiple angle brackets
if (firstOpenAngleBracketIdx != lastOpenAngleBracketIdx)
{
//Remove the multiple angle brackets surrounding the email address from the input string leaving just a single set
input = input.Substring(0, firstOpenAngleBracketIdx) + //Part before any angle brackets (display name if there is one)
input.Substring(lastOpenAngleBracketIdx, firstCloseAngleBracketIdx - lastOpenAngleBracketIdx + 1); //actual email address, including one angle bracket either side
}
// Find the location of the email address
var indexStartEmail = input.LastIndexOf('<');
var indexEndEmail = input.LastIndexOf('>');
try
{
if (indexStartEmail >= 0 && indexEndEmail >= 0)
{
// Check if there is a username in front of the email address
var username = indexStartEmail > 0 ? input.Substring(0, indexStartEmail).Trim() : string.Empty;
// Parse out the email address without the "<" and ">"
indexStartEmail = indexStartEmail + 1;
var emailLength = indexEndEmail - indexStartEmail;
var emailAddress = input.Substring(indexStartEmail, emailLength).Trim();
// There has been cases where there was no emailaddress between the < and >
if (!string.IsNullOrEmpty(emailAddress))
{
// If the username is quoted, MailAddress' constructor will remove them for us
return new RfcMailAddress(new MailAddress(emailAddress, username), input);
}
}
// This might be on the form noreply@mail.eksperten.dk
// Check if there is an email, if notm there is no need to try
if (input.Contains("@"))
return new RfcMailAddress(new MailAddress(input), input);
}
catch (FormatException)
{
// Sometimes invalid emails are sent, like sqlmap-user@sourceforge.net. (last period is illigal)
}
// It could be that the format used was simply a name
// which is indeed valid according to the RFC
// Example:
// Eksperten mailrobot
return new RfcMailAddress(input);
}
/// <summary>
/// Parses input of the form<br/>
/// <c>Eksperten mailrobot &lt;noreply@mail.eksperten.dk&gt;, ...</c><br/>
/// to a list of RFCMailAddresses
/// </summary>
/// <param name="input">The input that is a comma-separated list of EmailAddresses to parse</param>
/// <returns>A List of <seealso cref="RfcMailAddress"/> objects extracted from the <paramref name="input"/> parameter.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="input"/> is <see langword="null"/></exception>
internal static List<RfcMailAddress> ParseMailAddresses(string input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
// MailAddresses are split by commas
IEnumerable<string> mailAddresses = Utility.SplitStringWithCharNotInsideQuotes(input, ',');
// Parse each of these
return mailAddresses.Select(ParseMailAddress).ToList();
}
#endregion
}
}