OpenSim.Modules.EMail/src/MailKit/Envelope.cs

592 lines
15 KiB
C#

//
// Envelope.cs
//
// Author: Jeffrey Stedfast <jestedfa@microsoft.com>
//
// Copyright (c) 2013-2020 .NET Foundation and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
using System;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using MimeKit;
using MimeKit.Utils;
namespace MailKit {
/// <summary>
/// A message envelope containing a brief summary of the message.
/// </summary>
/// <remarks>
/// The envelope of a message contains information such as the
/// date the message was sent, the subject of the message,
/// the sender of the message, who the message was sent to,
/// which message(s) the message may be in reply to,
/// and the message id.
/// </remarks>
public class Envelope
{
/// <summary>
/// Initializes a new instance of the <see cref="MailKit.Envelope"/> class.
/// </summary>
/// <remarks>
/// Creates a new <see cref="Envelope"/>.
/// </remarks>
public Envelope ()
{
From = new InternetAddressList ();
Sender = new InternetAddressList ();
ReplyTo = new InternetAddressList ();
To = new InternetAddressList ();
Cc = new InternetAddressList ();
Bcc = new InternetAddressList ();
}
/// <summary>
/// Gets the address(es) that the message is from.
/// </summary>
/// <remarks>
/// Gets the address(es) that the message is from.
/// </remarks>
/// <value>The address(es) that the message is from.</value>
public InternetAddressList From {
get; private set;
}
/// <summary>
/// Gets the actual sender(s) of the message.
/// </summary>
/// <remarks>
/// The senders may differ from the addresses in <see cref="From"/> if
/// the message was sent by someone on behalf of someone else.
/// </remarks>
/// <value>The actual sender(s) of the message.</value>
public InternetAddressList Sender {
get; private set;
}
/// <summary>
/// Gets the address(es) that replies should be sent to.
/// </summary>
/// <remarks>
/// The senders of the message may prefer that replies are sent
/// somewhere other than the address they used to send the message.
/// </remarks>
/// <value>The address(es) that replies should be sent to.</value>
public InternetAddressList ReplyTo {
get; private set;
}
/// <summary>
/// Gets the list of addresses that the message was sent to.
/// </summary>
/// <remarks>
/// Gets the list of addresses that the message was sent to.
/// </remarks>
/// <value>The address(es) that the message was sent to.</value>
public InternetAddressList To {
get; private set;
}
/// <summary>
/// Gets the list of addresses that the message was carbon-copied to.
/// </summary>
/// <remarks>
/// Gets the list of addresses that the message was carbon-copied to.
/// </remarks>
/// <value>The address(es) that the message was carbon-copied to.</value>
public InternetAddressList Cc {
get; private set;
}
/// <summary>
/// Gets the list of addresses that the message was blind-carbon-copied to.
/// </summary>
/// <remarks>
/// Gets the list of addresses that the message was blind-carbon-copied to.
/// </remarks>
/// <value>The address(es) that the message was carbon-copied to.</value>
public InternetAddressList Bcc {
get; private set;
}
/// <summary>
/// The Message-Id that the message is replying to.
/// </summary>
/// <remarks>
/// The Message-Id that the message is replying to.
/// </remarks>
/// <value>The Message-Id that the message is replying to.</value>
public string InReplyTo {
get; set;
}
/// <summary>
/// Gets the date that the message was sent on, if available.
/// </summary>
/// <remarks>
/// Gets the date that the message was sent on, if available.
/// </remarks>
/// <value>The date the message was sent.</value>
public DateTimeOffset? Date {
get; set;
}
/// <summary>
/// Gets the ID of the message, if available.
/// </summary>
/// <remarks>
/// Gets the ID of the message, if available.
/// </remarks>
/// <value>The message identifier.</value>
public string MessageId {
get; set;
}
/// <summary>
/// Gets the subject of the message.
/// </summary>
/// <remarks>
/// Gets the subject of the message.
/// </remarks>
/// <value>The subject.</value>
public string Subject {
get; set;
}
static void EncodeMailbox (StringBuilder builder, MailboxAddress mailbox)
{
builder.Append ('(');
if (mailbox.Name != null)
builder.AppendFormat ("{0} ", MimeUtils.Quote (mailbox.Name));
else
builder.Append ("NIL ");
if (mailbox.Route.Count != 0)
builder.AppendFormat ("\"{0}\" ", mailbox.Route);
else
builder.Append ("NIL ");
int at = mailbox.Address.LastIndexOf ('@');
if (at >= 0) {
var domain = mailbox.Address.Substring (at + 1);
var user = mailbox.Address.Substring (0, at);
builder.AppendFormat ("{0} {1}", MimeUtils.Quote (user), MimeUtils.Quote (domain));
} else {
builder.AppendFormat ("{0} \"localhost\"", MimeUtils.Quote (mailbox.Address));
}
builder.Append (')');
}
static void EncodeInternetAddressListAddresses (StringBuilder builder, InternetAddressList addresses)
{
foreach (var addr in addresses) {
var mailbox = addr as MailboxAddress;
var group = addr as GroupAddress;
if (mailbox != null)
EncodeMailbox (builder, mailbox);
else if (group != null)
EncodeGroup (builder, group);
}
}
static void EncodeGroup (StringBuilder builder, GroupAddress group)
{
builder.AppendFormat ("(NIL NIL {0} NIL)", MimeUtils.Quote (group.Name));
EncodeInternetAddressListAddresses (builder, group.Members);
builder.Append ("(NIL NIL NIL NIL)");
}
static void EncodeAddressList (StringBuilder builder, InternetAddressList list)
{
builder.Append ('(');
EncodeInternetAddressListAddresses (builder, list);
builder.Append (')');
}
internal void Encode (StringBuilder builder)
{
builder.Append ('(');
if (Date.HasValue)
builder.AppendFormat ("\"{0}\" ", DateUtils.FormatDate (Date.Value));
else
builder.Append ("NIL ");
if (Subject != null)
builder.AppendFormat ("{0} ", MimeUtils.Quote (Subject));
else
builder.Append ("NIL ");
if (From.Count > 0) {
EncodeAddressList (builder, From);
builder.Append (' ');
} else {
builder.Append ("NIL ");
}
if (Sender.Count > 0) {
EncodeAddressList (builder, Sender);
builder.Append (' ');
} else {
builder.Append ("NIL ");
}
if (ReplyTo.Count > 0) {
EncodeAddressList (builder, ReplyTo);
builder.Append (' ');
} else {
builder.Append ("NIL ");
}
if (To.Count > 0) {
EncodeAddressList (builder, To);
builder.Append (' ');
} else {
builder.Append ("NIL ");
}
if (Cc.Count > 0) {
EncodeAddressList (builder, Cc);
builder.Append (' ');
} else {
builder.Append ("NIL ");
}
if (Bcc.Count > 0) {
EncodeAddressList (builder, Bcc);
builder.Append (' ');
} else {
builder.Append ("NIL ");
}
if (InReplyTo != null) {
if (InReplyTo.Length > 1 && InReplyTo[0] != '<' && InReplyTo[InReplyTo.Length - 1] != '>')
builder.AppendFormat ("{0} ", MimeUtils.Quote ('<' + InReplyTo + '>'));
else
builder.AppendFormat ("{0} ", MimeUtils.Quote (InReplyTo));
} else
builder.Append ("NIL ");
if (MessageId != null) {
if (MessageId.Length > 1 && MessageId[0] != '<' && MessageId[MessageId.Length - 1] != '>')
builder.AppendFormat ("{0}", MimeUtils.Quote ('<' + MessageId + '>'));
else
builder.AppendFormat ("{0}", MimeUtils.Quote (MessageId));
} else
builder.Append ("NIL");
builder.Append (')');
}
/// <summary>
/// Returns a <see cref="System.String"/> that represents the current <see cref="MailKit.Envelope"/>.
/// </summary>
/// <remarks>
/// <para>The returned string can be parsed by <see cref="TryParse(string,out Envelope)"/>.</para>
/// <note type="warning">The syntax of the string returned, while similar to IMAP's ENVELOPE syntax,
/// is not completely compatible.</note>
/// </remarks>
/// <returns>A <see cref="System.String"/> that represents the current <see cref="MailKit.Envelope"/>.</returns>
public override string ToString ()
{
var builder = new StringBuilder ();
Encode (builder);
return builder.ToString ();
}
static bool TryParse (string text, ref int index, out string nstring)
{
nstring = null;
while (index < text.Length && text[index] == ' ')
index++;
if (index >= text.Length)
return false;
if (text[index] != '"') {
if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") {
index += 3;
return true;
}
return false;
}
var token = new StringBuilder ();
bool escaped = false;
index++;
while (index < text.Length) {
if (text[index] == '"' && !escaped)
break;
if (escaped || text[index] != '\\') {
token.Append (text[index]);
escaped = false;
} else {
escaped = true;
}
index++;
}
if (index >= text.Length)
return false;
nstring = token.ToString ();
index++;
return true;
}
static bool TryParse (string text, ref int index, out InternetAddress addr)
{
string name, route, user, domain;
DomainList domains;
addr = null;
if (text[index] != '(')
return false;
index++;
if (!TryParse (text, ref index, out name))
return false;
if (!TryParse (text, ref index, out route))
return false;
if (!TryParse (text, ref index, out user))
return false;
if (!TryParse (text, ref index, out domain))
return false;
while (index < text.Length && text[index] == ' ')
index++;
if (index >= text.Length || text[index] != ')')
return false;
index++;
if (domain != null) {
var address = user + "@" + domain;
if (route != null && DomainList.TryParse (route, out domains))
addr = new MailboxAddress (name, domains, address);
else
addr = new MailboxAddress (name, address);
} else if (user != null) {
addr = new GroupAddress (user);
}
return true;
}
static bool TryParse (string text, ref int index, out InternetAddressList list)
{
list = null;
while (index < text.Length && text[index] == ' ')
index++;
if (index >= text.Length)
return false;
if (text[index] != '(') {
if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") {
list = new InternetAddressList ();
index += 3;
return true;
}
return false;
}
index++;
if (index >= text.Length)
return false;
list = new InternetAddressList ();
var stack = new List<InternetAddressList> ();
int sp = 0;
stack.Add (list);
do {
if (text[index] == ')')
break;
if (!TryParse (text, ref index, out InternetAddress addr))
return false;
if (addr != null) {
var group = addr as GroupAddress;
stack[sp].Add (addr);
if (group != null) {
stack.Add (group.Members);
sp++;
}
} else if (sp > 0) {
stack.RemoveAt (sp);
sp--;
}
while (index < text.Length && text[index] == ' ')
index++;
} while (index < text.Length);
// Note: technically, we should check that sp == 0 as well, since all groups should
// be popped off the stack, but in the interest of being liberal in what we accept,
// we'll ignore that.
if (index >= text.Length)
return false;
index++;
return true;
}
internal static bool TryParse (string text, ref int index, out Envelope envelope)
{
InternetAddressList from, sender, replyto, to, cc, bcc;
string inreplyto, messageid, subject, nstring;
DateTimeOffset? date = null;
envelope = null;
while (index < text.Length && text[index] == ' ')
index++;
if (index >= text.Length || text[index] != '(') {
if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") {
index += 3;
return true;
}
return false;
}
index++;
if (!TryParse (text, ref index, out nstring))
return false;
if (nstring != null) {
DateTimeOffset value;
if (!DateUtils.TryParse (nstring, out value))
return false;
date = value;
}
if (!TryParse (text, ref index, out subject))
return false;
if (!TryParse (text, ref index, out from))
return false;
if (!TryParse (text, ref index, out sender))
return false;
if (!TryParse (text, ref index, out replyto))
return false;
if (!TryParse (text, ref index, out to))
return false;
if (!TryParse (text, ref index, out cc))
return false;
if (!TryParse (text, ref index, out bcc))
return false;
if (!TryParse (text, ref index, out inreplyto))
return false;
if (!TryParse (text, ref index, out messageid))
return false;
if (index >= text.Length || text[index] != ')')
return false;
index++;
envelope = new Envelope {
Date = date,
Subject = subject,
From = from,
Sender = sender,
ReplyTo = replyto,
To = to,
Cc = cc,
Bcc = bcc,
InReplyTo = inreplyto != null ? MimeUtils.EnumerateReferences (inreplyto).FirstOrDefault () ?? inreplyto : null,
MessageId = messageid != null ? MimeUtils.EnumerateReferences (messageid).FirstOrDefault () ?? messageid : null
};
return true;
}
/// <summary>
/// Tries to parse the given text into a new <see cref="MailKit.Envelope"/> instance.
/// </summary>
/// <remarks>
/// <para>Parses an Envelope value from the specified text.</para>
/// <note type="warning">This syntax, while similar to IMAP's ENVELOPE syntax, is not
/// completely compatible.</note>
/// </remarks>
/// <returns><c>true</c>, if the envelope was successfully parsed, <c>false</c> otherwise.</returns>
/// <param name="text">The text to parse.</param>
/// <param name="envelope">The parsed envelope.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="text"/> is <c>null</c>.
/// </exception>
public static bool TryParse (string text, out Envelope envelope)
{
if (text == null)
throw new ArgumentNullException (nameof (text));
int index = 0;
return TryParse (text, ref index, out envelope) && index == text.Length;
}
}
}